diff --git a/example.env b/example.env index 89f58881af28ad4a838b10bb4c7fb3540e38313a..312615c539623859e2099553a9dac7c023814b4e 100644 --- a/example.env +++ b/example.env @@ -3,9 +3,9 @@ # Are advanced & experimental features enabled? # ADVANCED=false -# Public HTTP(S) URL of LexImpact Socio-Fiscal API server -API_BASE_URL="https://simulateur-socio-fiscal.leximpact.dev/api/" -# API_BASE_URL="http://localhost:8000" +# Public HTTP(S) URLs of LexImpact Socio-Fiscal API server +API_BASE_URLS="https://simulateur-socio-fiscal.leximpact.dev/api/" +# API_BASE_URLS="http://localhost:8000" # Public HTTP(S) URL of LexImpact Socio-Fiscal UI server (aka this application) # BASE_URL="https://simulateur-socio-fiscal.leximpact.dev/" diff --git a/src/hooks.ts b/src/hooks.ts index b1eb8ac8115fceec38c80bae8c95f85e264d9ce3..5b9859773c1c16ef6e8777a5b966477ddd31fa70 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -29,19 +29,21 @@ if (globalThis.fetch === undefined) { Headers: { enumerable: true, value: Headers, - } + }, }) } - const { githubPersonalAccessToken, openfiscaRepository } = config export const getSession: GetSession<{}, Session> = async (request) => { - const { user } = oauth2Authenticator === undefined ? { user: undefined } : await oauth2Authenticator.getSession(request) + const { user } = + oauth2Authenticator === undefined + ? { user: undefined } + : await oauth2Authenticator.getSession(request) return { advanced: config.advanced, - apiBaseUrl: config.apiBaseUrl, - apiWebSocketBaseUrl: config.apiWebSocketBaseUrl, + apiBaseUrls: config.apiBaseUrls, + apiWebSocketBaseUrls: config.apiWebSocketBaseUrls, authenticationEnabled: oauth2Authenticator !== undefined, baseUrl: config.baseUrl, childrenKey: config.childrenKey, @@ -72,8 +74,8 @@ export const getSession: GetSession<{}, Session> = async (request) => { export const handle: Handle = async ({ request, resolve }) => { // TODO https://github.com/sveltejs/kit/issues/1046 if (request.query.has("_method")) { - request.method = request.query.get("_method").toUpperCase(); + request.method = request.query.get("_method").toUpperCase() } return await resolve(request) -}; +} diff --git a/src/lib/auditors/config.ts b/src/lib/auditors/config.ts index dc0c3d7244184a12b7d476b239874bd5d0e01037..7db652ef2280e468c2cb12109c4daf5b83102ec0 100644 --- a/src/lib/auditors/config.ts +++ b/src/lib/auditors/config.ts @@ -1,7 +1,7 @@ import { Audit, - auditArray, auditBoolean, + auditCleanArray, auditFunction, auditHttpUrl, auditInteger, @@ -42,7 +42,19 @@ export function auditConfig( auditSetNullish(false), ) } - for (const key of ["apiBaseUrl", "baseUrl", "portalUrl"]) { + audit.attribute( + data, + "apiBaseUrls", + true, + errors, + remainingKeys, + auditString, + auditFunction((keys) => keys.split(",")), + auditCleanArray(auditHttpUrl), + auditUnique, + auditRequire, + ) + for (const key of ["baseUrl", "portalUrl"]) { audit.attribute( data, key, @@ -87,17 +99,10 @@ export function auditConfig( remainingKeys, auditString, auditFunction((keys) => keys.split(",")), - auditArray(auditTrimString), + auditCleanArray(auditTrimString), auditUnique, ) - audit.attribute( - data, - "matomo", - true, - errors, - remainingKeys, - auditMatomo, - ) + audit.attribute(data, "matomo", true, errors, remainingKeys, auditMatomo) audit.attribute( data, "openfiscaRepository", @@ -107,20 +112,12 @@ export function auditConfig( auditOpenFiscaRepository, auditRequire, ) - audit.attribute( - data, - "oauth2", - true, - errors, - remainingKeys, - auditOauth2, - ) + audit.attribute(data, "oauth2", true, errors, remainingKeys, auditOauth2) return audit.reduceRemaining(data, errors, remainingKeys) } -function auditMatomo(audit: Audit, dataUnknown: unknown, -): [unknown, unknown] { +function auditMatomo(audit: Audit, dataUnknown: unknown): [unknown, unknown] { if (dataUnknown == null) { return [dataUnknown, null] } @@ -167,15 +164,14 @@ function auditMatomo(audit: Audit, dataUnknown: unknown, remainingKeys, auditHttpUrl, // Ensure there is a single trailing "/" in URL. - auditFunction(url => url.replace(/\/$/, "") + "/"), + auditFunction((url) => url.replace(/\/$/, "") + "/"), auditRequire, ) return audit.reduceRemaining(data, errors, remainingKeys) } -function auditOauth2(audit: Audit, dataUnknown: unknown, -): [unknown, unknown] { +function auditOauth2(audit: Audit, dataUnknown: unknown): [unknown, unknown] { if (dataUnknown == null) { return [dataUnknown, null] } diff --git a/src/lib/components/latchkeys/Arrow.svelte b/src/lib/components/latchkeys/Arrow.svelte index 32310637283f4d4634688cf15882c77d15fb8e55..c110a0ca0565d86935509cb35c104b92dc038ba0 100644 --- a/src/lib/components/latchkeys/Arrow.svelte +++ b/src/lib/components/latchkeys/Arrow.svelte @@ -34,9 +34,9 @@ const chevronStrokeWidth = 5 const chevronStrokeHalfWidth = chevronStrokeWidth / 2 // const date = new Date().toISOString().split("T")[0] - const decompositionByNameArray = getContext( - "decompositionByNameArray", - ) as Writable<DecompositionByName[]> + const decompositionByName = getContext( + "decompositionByName", + ) as Writable<DecompositionByName> const deltaFormatter = new Intl.NumberFormat("fr-FR", { currency: "EUR", maximumFractionDigits: 0, @@ -99,38 +99,32 @@ : xAggregate1 function toggle() { - if ( - aggregate === undefined || - (aggregate !== leaf && aggregate.values.every(([x0]) => x0 === 0)) - ) { + if (aggregate === undefined || (aggregate !== leaf && aggregate.trunk)) { return } const open = !aggregate.open let smallestAggregate = aggregate const index = $testCaseIndex - const decompositionByName = $decompositionByNameArray[index] if (open) { // When aggregate has just one child, we need to also open // its child... and so on. while (smallestAggregate.childrenName?.length === 1) { smallestAggregate = - decompositionByName[smallestAggregate.childrenName[0]] + $decompositionByName[smallestAggregate.childrenName[0]] } } - const newDecompositionByNameArray = [...$decompositionByNameArray] - newDecompositionByNameArray[index] = toggleToNode( - { ...decompositionByName }, - decompositionByName[rootDecompositionName], + $decompositionByName = toggleToNode( + { ...$decompositionByName }, + $decompositionByName[rootDecompositionName], [ ...iterDecompositionAncestorsName( - decompositionByName, + $decompositionByName, smallestAggregate.name, ), ].slice(1), open, ) - $decompositionByNameArray = newDecompositionByNameArray } function toggleToNode( diff --git a/src/lib/components/latchkeys/Latchkey.svelte b/src/lib/components/latchkeys/Latchkey.svelte index 2ba9418b5e382ebf18bc0b2c821ea07cab2fb5ce..f643eb645cb15a4d182bc2b69801d67acdc676e5 100644 --- a/src/lib/components/latchkeys/Latchkey.svelte +++ b/src/lib/components/latchkeys/Latchkey.svelte @@ -139,10 +139,7 @@ latestChildDataItem.aggregate.deltaAtVectorIndex === decomposition.deltaAtVectorIndex ) { - if ( - decomposition.open || - decomposition.values.every(([x0]) => x0 === 0) - ) { + if (decomposition.open || decomposition.trunk) { return { aggregate: decomposition, leaf: latestChildDataItem.leaf, diff --git a/src/lib/components/scholar_waterfalls/ScholarWaterfall.svelte b/src/lib/components/scholar_waterfalls/ScholarWaterfall.svelte index 08125318559f923aeab3464ca5ad90180f104e58..7855b428a84d82913e60680798e60bbbd957180f 100644 --- a/src/lib/components/scholar_waterfalls/ScholarWaterfall.svelte +++ b/src/lib/components/scholar_waterfalls/ScholarWaterfall.svelte @@ -5,13 +5,19 @@ import { session } from "$app/stores" import PictoFemme from "$lib/components/pictos/PictoFemme.svelte" import PictoEntreprise from "$lib/components/pictos/PictoEntreprise.svelte" - import type { Decomposition, DecompositionByName } from "$lib/decompositions" + import type { + Decomposition, + DecompositionByName, + Evaluation, + EvaluationByName, + } from "$lib/decompositions" import { iterDecompositionChildren, walkDecompositions, } from "$lib/decompositions" export let decompositionByName: DecompositionByName + export let evaluationByName: EvaluationByName const adaptAmountsScale = getContext("adaptAmountsScale") as Writable<boolean> const advanced = $session.advanced @@ -35,9 +41,10 @@ const testCaseIndex = getContext("testCaseIndex") as Writable<number> let waterfallWidth = 100 - $: nodeAndDepthCouples = [ - ...walkVisibleDecompositions( + $: decompositionEvaluationAndDepthTriples = [ + ...walkVisibleEvaluations( decompositionByName, + evaluationByName, rootDecompositionName, $showNulls, ), @@ -45,6 +52,7 @@ $: [valueMin, valueMax] = computeXDomain( decompositionByName, + evaluationByName, rootDecompositionName, $adaptAmountsScale, ) @@ -53,19 +61,24 @@ function computeXDomain( decompositionByName: DecompositionByName, + evaluationByName: EvaluationByName, name: string, adaptAmountsScale: boolean, ): [number, number] { let valueMin = undefined let valueMax = undefined - for (const node of walkDecompositions( + for (const decomposition of walkDecompositions( decompositionByName, name, false, false, )) { + const evaluation = evaluationByName[decomposition.name] + if (evaluation === undefined) { + continue + } if (adaptAmountsScale) { - for (const value of node.valuesAtVectorIndex ?? []) { + for (const value of evaluation.valuesAtVectorIndex ?? []) { if (valueMin === undefined || value < valueMin) { valueMin = value } @@ -74,7 +87,7 @@ } } } else { - for (const itemValues of node.values) { + for (const itemValues of evaluation.values) { for (const value of itemValues) { if (valueMin === undefined || value < valueMin) { valueMin = value @@ -97,38 +110,44 @@ function togglableParentNode( decompositionByName: DecompositionByName, - node: Decomposition, + decomposition: Decomposition, ): boolean { - if (node.parentName === undefined) { + if (decomposition.parentName === undefined) { return false } - const parent = decompositionByName[node.parentName] + const parent = decompositionByName[decomposition.parentName] if (parent === undefined) { return false } - return parent.valuesX0Is0Array && parent.childrenName.length === 1 + return parent.trunk && parent.childrenName.length === 1 } - export function* walkVisibleDecompositions( + export function* walkVisibleEvaluations( decompositionByName: DecompositionByName, + evaluationByName: EvaluationByName, name: string, showNulls: boolean, depth: number = 0, - ): Generator<[Decomposition, number], void, unknown> { + ): Generator<[Decomposition, Evaluation, number], void, unknown> { const decomposition = decompositionByName[name] if (decomposition === undefined) { return } - const showNode = showNulls || !decomposition.deltaIs0Array + const evaluation = evaluationByName[name] + if (evaluation === undefined) { + return + } + const showNode = showNulls || !evaluation.deltaIs0Array let childrenDepth = depth - if (showNode && !decomposition.valuesX0Is0Array) { - yield [decomposition, depth] + if (showNode && !decomposition.trunk) { + yield [decomposition, evaluation, depth] childrenDepth = depth + 1 } if (decomposition.childrenName !== undefined && decomposition.open) { for (const childName of decomposition.childrenName) { - yield* walkVisibleDecompositions( + yield* walkVisibleEvaluations( decompositionByName, + evaluationByName, childName, showNulls, childrenDepth, @@ -137,10 +156,10 @@ } if ( showNode && - decomposition.valuesX0Is0Array && + decomposition.trunk && (!decomposition.open || decomposition.childrenName.length > 1) ) { - yield [decomposition, depth] + yield [decomposition, evaluation, depth] } } @@ -199,13 +218,10 @@ } decomposition = { ...decomposition } if (decomposition.open) { - if ( - decomposition.childrenName.length === 1 && - decomposition.values.every(([x0]) => x0 === 0) - ) { - // When aggregate is absolute and has only one child, instead of closing node, - // replace its child with the children of its child, - // We do this, because we don't want to hide every node before this one. + if (decomposition.childrenName.length === 1 && decomposition.trunk) { + // When aggregate is absolute and has only one child, instead of closing + //decomposition, replace its child with the children of its child, + // We do this, because we don't want to hide every decomposition before this one. const child = decompositionByName[decomposition.childrenName[0]] if (child !== undefined) { if (child.childrenName !== undefined) { @@ -243,22 +259,22 @@ } </script> -{#if nodeAndDepthCouples.length > 0} +{#if decompositionEvaluationAndDepthTriples.length > 0} <div class="flex pr-2"> <!-- partie gauche dispositifs et ticket de caisse--> <div class="bg-opacity-40 shadow-lg w-4/5"> <div class="flex justify-between"> <!-- Navigation dispositifs--> <div class="flex-auto pl-2 pt-2 w-3/5"> - {#each nodeAndDepthCouples as [node, depth], index} + {#each decompositionEvaluationAndDepthTriples as [decomposition, _evaluation, depth], index} <div class="flex items-center h-8 whitespace-nowrap"> {#each [...iterToDepth(depth)] as _level} <div class="border-l-2 border-le-gris-dispositif h-8 pl-1"> </div> {/each} - {#if node.open || index === 0} - {#if node.valuesX0Is0Array} + {#if decomposition.open || index === 0} + {#if decomposition.trunk} <div class="hover:bg-white border-gray-400 overflow-ellipsis overflow-x-hidden hover:overflow-x-visible text-gray-400 hover:z-20" class:border-t={index !== 0} @@ -266,24 +282,25 @@ {#if advanced} <span class="cursor-pointer text-base hover:underline" - on:click={() => dispatch("selectVariable", node.name)} - >{node.label}</span + on:click={() => + dispatch("selectVariable", decomposition.name)} + >{decomposition.label}</span > {:else} - <span class="text-sm">{node.label}</span> + <span class="text-sm">{decomposition.label}</span> {/if} </div> {:else} <div class="hover:bg-white cursor-pointer hover:font-bold font-serif overflow-ellipsis overflow-x-hidden hover:overflow-x-visible text-le-gris-dispositif text-base hover:underline hover:z-20" - on:click={() => zoomOut(node.name)} + on:click={() => zoomOut(decomposition.name)} > - {node.label} + {decomposition.label} </div> {/if} {:else} <div> - {#if node.lastReview === undefined || node.lastReview < "2020"} + {#if decomposition.lastReview === undefined || decomposition.lastReview < "2020"} <!-- Inspired from Material Icons name: Warning / with white symbol inside --> <svg aria-hidden="true" @@ -292,10 +309,10 @@ xmlns="http://www.w3.org/2000/svg" > <title - >Dernière relecture : {#if node.lastReview === undefined}<i + >Dernière relecture : {#if decomposition.lastReview === undefined}<i >date indéterminée</i >{:else}{dateFormatter.format( - new Date(node.lastReview), + new Date(decomposition.lastReview), )}{/if}</title > <path @@ -321,7 +338,7 @@ > <title >Dernière relecture : {dateFormatter.format( - new Date(node.lastReview), + new Date(decomposition.lastReview), )}</title > <path @@ -340,19 +357,19 @@ class="hover:bg-white cursor-pointer font-serif overflow-ellipsis overflow-x-hidden hover:overflow-x-visible hover:text-le-gris-dispositif text-base hover:underline hover:z-20" on:click={() => { if ( - node.childrenName !== undefined && - !node.valuesX0Is0Array + decomposition.childrenName !== undefined && + !decomposition.trunk ) { - zoomIn(node.name) + zoomIn(decomposition.name) } - dispatch("selectVariable", node.name) - }}>{node.label}</span + dispatch("selectVariable", decomposition.name) + }}>{decomposition.label}</span > {/if} - {#if node.open && !node.valuesX0Is0Array} + {#if decomposition.open && !decomposition.trunk} <button class="text-le-gris-dispositif-dark ml-1 px-1 rounded-full hover:border-2" - on:click={() => zoomOut(node.name)} + on:click={() => zoomOut(decomposition.name)} ><div> <!--Material Icon Expand less--><svg class="fill-current" @@ -367,10 +384,10 @@ > </div></button > - {:else if togglableParentNode(decompositionByName, node)} + {:else if togglableParentNode(decompositionByName, decomposition)} <button class="text-le-gris-dispositif-dark ml-1 px-1 rounded-full hover:border-2" - on:click={() => zoomOut(node.parentName)} + on:click={() => zoomOut(decomposition.parentName)} ><div> <!--Material Icon Expand less--><svg class="fill-current" @@ -386,10 +403,10 @@ </div></button > {/if} - {#if (!node.open && node.childrenName !== undefined) || node.previousChildStack !== undefined} + {#if (!decomposition.open && decomposition.childrenName !== undefined) || decomposition.previousChildStack !== undefined} <button class="hover:border-2 text-le-gris-dispositif-dark ml-1 px-1 rounded-full " - on:click={() => zoomIn(node.name)} + on:click={() => zoomIn(decomposition.name)} ><div> <svg class="fill-current" @@ -411,9 +428,9 @@ <!-- ticket de caisse--> <div class="flex flex-none pt-2 px-2"> <div class="flex-col"> - {#each nodeAndDepthCouples as [node, depth], index} + {#each decompositionEvaluationAndDepthTriples as [decomposition, _evaluation, depth], index} <div class="border-transparent border-t h-7 my-1 px-1 text-sm"> - {#if depth === 0 && !node.valuesX0Is0Array && index > 0}{#if node.involve === "person"}<span + {#if depth === 0 && !decomposition.trunk && index > 0}{#if decomposition.involve === "person"}<span class="text-red-600 font-bold" > <PictoFemme /></span @@ -426,43 +443,45 @@ {/each} </div> <div class="flex-col "> - {#each nodeAndDepthCouples as [node, _depth], index} + {#each decompositionEvaluationAndDepthTriples as [decomposition, evaluation, _depth], index} <div - class="{node.open && - node.valuesX0Is0Array && - node.childrenName.length > 1 + class="{decomposition.open && + decomposition.trunk && + decomposition.childrenName.length > 1 ? 'border-gray-400' : 'border-transparent'} border-t h-7 my-1 px-1 text-sm" > - {#if node.open}{#if node.valuesX0Is0Array}<span + {#if decomposition.open}{#if decomposition.trunk}<span class="text-gray-400">=</span - >{/if}{:else if index > 0}{#if node.deltaAtVectorIndex < 0}<span + >{/if}{:else if index > 0}{#if evaluation.deltaAtVectorIndex < 0}<span class="text-red-600 font-bold">-</span - >{:else if node.deltaAtVectorIndex > 0}<span + >{:else if evaluation.deltaAtVectorIndex > 0}<span class="text-green-600 font-bold ">+</span >{/if}{/if} </div> {/each} </div> <div class="flex-col"> - {#each nodeAndDepthCouples as [node, _depth], index} + {#each decompositionEvaluationAndDepthTriples as [decomposition, evaluation, _depth], index} <div - class="{node.open && - node.valuesX0Is0Array && - node.childrenName.length > 1 + class="{decomposition.open && + decomposition.trunk && + decomposition.childrenName.length > 1 ? 'border-gray-400' : 'border-transparent'} border-t h-7 my-1 text-right text-sm" > - {#if node.open || index === 0} - {#if node.valuesX0Is0Array} + {#if decomposition.open || index === 0} + {#if decomposition.trunk} <span class="text-gray-400 text-sm" >{firstDeltaFormatter.format( - node.deltaAtVectorIndex, + evaluation.deltaAtVectorIndex, )}</span > {/if} {:else}<span class="font-bold text-base" - >{deltaFormatter.format(node.deltaAtVectorIndex)}</span + >{deltaFormatter.format( + evaluation.deltaAtVectorIndex, + )}</span > {/if} </div> @@ -488,21 +507,23 @@ <!-- Waterfall--> <div class="flex flex-col pt-2 w-1/5" bind:offsetWidth={waterfallWidth}> - {#each nodeAndDepthCouples as [node, _depth], index} + {#each decompositionEvaluationAndDepthTriples as [decomposition, evaluation, _depth], index} <div - class="{node.open || index === 0 - ? node.valuesX0Is0Array + class="{decomposition.open || index === 0 + ? decomposition.trunk ? 'bg-gray-200' : 'bg-transparent' - : node.valuesAtVectorIndex[0] <= node.valuesAtVectorIndex[1] + : evaluation.valuesAtVectorIndex[0] <= + evaluation.valuesAtVectorIndex[1] ? 'bg-green-500' : 'bg-red-500'} border-t border-transparent h-6 my-1" - style="margin-left: {((Math.min(...node.valuesAtVectorIndex) - + style="margin-left: {((Math.min(...evaluation.valuesAtVectorIndex) - valueMin) / (valueMax - valueMin)) * widthMax}px; width: {Math.max( (Math.abs( - node.valuesAtVectorIndex[1] - node.valuesAtVectorIndex[0], + evaluation.valuesAtVectorIndex[1] - + evaluation.valuesAtVectorIndex[0], ) / (valueMax - valueMin)) * widthMax, diff --git a/src/lib/components/test_cases/TestCaseView.svelte b/src/lib/components/test_cases/TestCaseView.svelte index 6aded5277b20ce6d0fa568b6dfff982b48ea80f9..92470dadbc9d1f7d8a8e24878870de1ce89ca236 100644 --- a/src/lib/components/test_cases/TestCaseView.svelte +++ b/src/lib/components/test_cases/TestCaseView.svelte @@ -4,7 +4,10 @@ import { session } from "$app/stores" import ScholarWaterfall from "$lib/components/scholar_waterfalls/ScholarWaterfall.svelte" - import type { DecompositionByName } from "$lib/decompositions" + import type { + DecompositionByName, + EvaluationByName, + } from "$lib/decompositions" import { walkDecompositionsCoreName } from "$lib/decompositions" import type { Axis, Situation } from "$lib/situations" import PictoArbreMetropole from "../pictos/PictoArbreMetropole.svelte" @@ -22,6 +25,7 @@ import { retrieveVariableSummaryByName } from "$lib/variables" export let decompositionByName: DecompositionByName + export let evaluationByName: EvaluationByName // export let index: number export let open: boolean export let situation: Situation @@ -79,13 +83,7 @@ $: adultsCount = Object.keys(personSituation).length - childrenCount - $: stateCost = Object.values(decompositionByName).reduce( - (sum: number, { childrenName, deltaAtVectorIndex }, index) => - childrenName === undefined && index > 0 - ? sum + (deltaAtVectorIndex ?? 0) - : sum, - 0, - ) + $: stateCost = computeStateCost(decompositionByName, evaluationByName) $: updateVariablesName(situation) @@ -125,11 +123,25 @@ // dispatch("changeAxis", axis === null ? [] : [[axis]]) // } + function computeStateCost( + decompositionByName: DecompositionByName, + evaluationByName: EvaluationByName, + ): number { + return Object.entries(decompositionByName).reduce( + (sum: number, [name, decomposition], index) => + decomposition.childrenName === undefined && index > 0 + ? sum + (evaluationByName[name].deltaAtVectorIndex ?? 0) + : sum, + 0, + ) + } + function getVariableValue( situation: Situation, variableName: string, populationId: string, ): number | undefined { + console.log("getVariableValue", variableName, populationId) const variable = variableSummaryByName[variableName] if (variable === undefined) { return undefined @@ -350,10 +362,12 @@ {label} /an : </span> <span - >{#if allowSlider && isAxis(axisDescription, name, "Adulte 1")}{Math.round( - vectorIndex * axisDescription.stepValue, + >{#if allowSlider && isAxis(axisDescription, name, "Adulte 1")}{euroAmountFormatter.format( + Math.round(vectorIndex * axisDescription.stepValue), )}{:else} - {getVariableValue(situation, name, "Adulte 1")}{/if} €</span + {euroAmountFormatter.format( + getVariableValue(situation, name, "Adulte 1"), + )}{/if}</span > {#if allowSlider} <label class="text-xs"> @@ -406,6 +420,7 @@ {#if open} <ScholarWaterfall {decompositionByName} + {evaluationByName} on:changeDecompositionByName on:selectVariable /> @@ -450,7 +465,7 @@ title="⚠️ Les dispositifs n'étant pas tous à jour, ce montant est à considérer avec prudence" > {euroAmountFormatter.format( - decompositionByName[rootDecompositionName].deltaAtVectorIndex ?? 0, + evaluationByName[rootDecompositionName].deltaAtVectorIndex ?? 0, )} </p> </div> @@ -467,7 +482,7 @@ title="⚠️ Les dispositifs n'étant pas tous à jour, ce montant est à considérer avec prudence" > {euroAmountFormatter.format( - decompositionByName[firstDecompositionName].deltaAtVectorIndex ?? 0, + evaluationByName[firstDecompositionName].deltaAtVectorIndex ?? 0, )} </p> </div> diff --git a/src/lib/components/test_cases/TestCasesPane.svelte b/src/lib/components/test_cases/TestCasesPane.svelte index bc2d9e773b99d067c6925398bd98bf5d40b807d4..2d54b94db3e3abb908e97f75a1f846920bc0369e 100644 --- a/src/lib/components/test_cases/TestCasesPane.svelte +++ b/src/lib/components/test_cases/TestCasesPane.svelte @@ -4,7 +4,11 @@ import type { Writable } from "svelte/store" import { session } from "$app/stores" - import type { DecompositionByName } from "$lib/decompositions" + import type { + CalculationName, + DecompositionByName, + EvaluationByNameArrayByCalculationName, + } from "$lib/decompositions" import type { Axis, AxisDescription, Situation } from "$lib/situations" import { indexOfSituationPopulationId } from "$lib/situations" @@ -13,12 +17,16 @@ export let year: number let axisBySituationIndex: { [situationIndex: string]: Axis } = {} + let calculationName: CalculationName = "law" - const decompositionByNameArray = getContext( - "decompositionByNameArray", - ) as Writable<DecompositionByName[]> + const decompositionByName = getContext( + "decompositionByName", + ) as Writable<DecompositionByName> const dispatch = createEventDispatcher() const entityByKey = $session.entityByKey as EntityByKey + const evaluationByNameArrayByCalculationName = getContext( + "evaluationByNameArrayByCalculationName", + ) as Writable<EvaluationByNameArrayByCalculationName> const testCases = getContext("testCases") as Writable<Situation[]> const vectorIndexes = getContext("vectorIndexes") as Writable<number[]> @@ -65,11 +73,9 @@ function changeDecompositionByName( situationIndex: number, - decompositionByName: DecompositionByName, + newDecompositionByName: DecompositionByName, ): void { - const newDecompositionByNameArray = [...$decompositionByNameArray] - newDecompositionByNameArray[situationIndex] = decompositionByName - $decompositionByNameArray = newDecompositionByNameArray + $decompositionByName = newDecompositionByName } function changeSituation(situationIndex: number, situation: Situation): void { @@ -82,7 +88,10 @@ <section class=" grid grid-cols-1 md:grid-cols-2 gap-10"> {#each $testCases as situation, situationIndex} <TestCaseView - decompositionByName={$decompositionByNameArray[situationIndex]} + decompositionByName={$decompositionByName} + evaluationByName={$evaluationByNameArrayByCalculationName[ + calculationName + ][situationIndex]} on:changeAxis={({ detail }) => changeAxis(situationIndex, detail)} on:changeDecompositionByName={({ detail }) => changeDecompositionByName(situationIndex, detail)} diff --git a/src/lib/components/variables/FormulaView.svelte b/src/lib/components/variables/FormulaView.svelte index 5df37dbfd4f46cb86154eca3778134824025474a..bc68de41dc44981d055fd29abd48b6088d1fb07e 100644 --- a/src/lib/components/variables/FormulaView.svelte +++ b/src/lib/components/variables/FormulaView.svelte @@ -4,7 +4,6 @@ extractFromFormulaAst, newFormulaRepositoryUrl, } from "@openfisca/ast" - import type Sockette from "sockette" import { getContext } from "svelte" import type { Writable } from "svelte/store" @@ -13,6 +12,7 @@ import VariableInput from "$lib/components/variables/VariableInput.svelte" import type { Situation } from "$lib/situations" import { retrieveVariableSummaryByName } from "$lib/variables" + import type { WebSocketByName, WebSocketOpenByName } from "$lib/websockets" export let formula: Formula export let situation: Situation @@ -20,8 +20,12 @@ let variableSummaryByName: { [name: string]: Variable } | undefined = undefined - const webSocket = getContext("webSocket") as Writable<Sockette | undefined> - const webSocketOpen = getContext("webSocketOpen") as Writable<boolean> + const webSocketByName = getContext("webSocketByName") as Writable< + WebSocketByName | undefined + > + const webSocketOpenByName = getContext( + "webSocketOpenByName", + ) as Writable<WebSocketOpenByName> $: extraction = extractFromFormulaAst( formula.ast, @@ -31,7 +35,7 @@ $: metadata = $session.metadata - $: if (browser && $webSocketOpen) { + $: if (browser && $webSocketOpenByName.law) { calculateVariables(extraction.openFiscaVariablesName) } @@ -41,7 +45,7 @@ [...variablesName], ) - $webSocket.send( + $webSocketByName.law.send( JSON.stringify({ calculate: true, variables: Object.keys(variableSummaryByName), diff --git a/src/lib/decompositions.ts b/src/lib/decompositions.ts index 387d2e059aa636d606e9c8bba3a06fe6ac3aa323..1a82c0504325322460a83962e481e7893086702c 100644 --- a/src/lib/decompositions.ts +++ b/src/lib/decompositions.ts @@ -3,15 +3,14 @@ import type { DecompositionReference, } from "@openfisca/ast" +export type CalculationName = "amendment" | "bill" | "law" + export interface DecompositionCore extends DecompositionJson { lastReview?: string } export interface Decomposition { childrenName?: string[] - delta?: number[] - deltaAtVectorIndex?: number - deltaIs0Array?: boolean index: number involve: "organization" | "person" label: string @@ -21,26 +20,41 @@ export interface Decomposition { open?: boolean parentName?: string previousChildStack?: string[] - values?: [number, number][] - valuesAtVectorIndex?: [number, number] - valuesX0Is0Array?: boolean + trunk: boolean } export type DecompositionCoreByName = { [name: string]: DecompositionCore } export type DecompositionByName = { [name: string]: Decomposition } +export interface Evaluation { + delta: number[] + deltaAtVectorIndex: number + deltaIs0Array: boolean + values: [number, number][] + valuesAtVectorIndex: [number, number] +} + +export type EvaluationByName = { [name: string]: Evaluation } + +export type EvaluationByNameArrayByCalculationName = { + [name in CalculationName]?: EvaluationByName[] +} + export interface LatchkeyDataItem { aggregate?: Decomposition leaf: Decomposition } +export const calculationNames: CalculationName[] = ["law", "bill", "amendment"] + export function decompositionByNameFromCore( decompositionCoreByName: DecompositionCoreByName, name: string, parentName: string | undefined = undefined, index = 0, negate: boolean | undefined = undefined, + trunk = true, decompositionByName: DecompositionByName = {}, visitedNames = new Set<string>(), ): DecompositionByName | undefined { @@ -63,6 +77,7 @@ export function decompositionByNameFromCore( lastReview: decompositionCore.lastReview, name, negate, + trunk, } decompositionByName[name] = decomposition if (parentName !== undefined) { @@ -104,6 +119,7 @@ export function decompositionByNameFromCore( name, childIndex, childReference.negate, + trunk && index === 0, decompositionByName, visitedNames, ) @@ -142,18 +158,18 @@ export function* iterDecompositionChildren( } } -export function updateDecompositionEvaluations( +export function updateEvaluations( decompositionByName: DecompositionByName, + evaluationByName: EvaluationByName, name: string, - deltaByName: { [name: string]: number[] }, vectorIndex: number, vectorLength: number, - newDecompositionByName: DecompositionByName = {}, + newEvaluationByName: EvaluationByName = {}, valuesPrevious = undefined, -): DecompositionByName { +): EvaluationByName { const decomposition = decompositionByName[name] if (decomposition === undefined) { - return newDecompositionByName + return newEvaluationByName } if (valuesPrevious === undefined) { valuesPrevious = new Array(vectorLength).fill(0) @@ -162,87 +178,96 @@ export function updateDecompositionEvaluations( if (childrenName !== undefined) { let childValuesPrevious = valuesPrevious for (const childName of childrenName) { - updateDecompositionEvaluations( + updateEvaluations( decompositionByName, + evaluationByName, childName, - deltaByName, vectorIndex, vectorLength, - newDecompositionByName, + newEvaluationByName, childValuesPrevious, ) - const child = newDecompositionByName[childName] - childValuesPrevious = child.values.map((itemValue) => itemValue[1]) + const childEvaluation = newEvaluationByName[childName] + childValuesPrevious = childEvaluation.values.map( + (itemValue) => itemValue[1], + ) } } - let delta = deltaByName[name] - if (delta === undefined) { + let evaluation = evaluationByName[name] + let delta + if (evaluation === undefined) { if (childrenName === undefined) { delta = new Array(vectorLength).fill(0) } else { - const firstChildValues = newDecompositionByName[childrenName[0]].values + const firstChildValues = newEvaluationByName[childrenName[0]].values const lastChildValues = - newDecompositionByName[childrenName[childrenName.length - 1]].values + newEvaluationByName[childrenName[childrenName.length - 1]].values delta = lastChildValues.map( (lastChildValue, index) => lastChildValue[1] - firstChildValues[index][0], ) + if (decomposition.negate) { + delta = delta.map((deltaItem) => -deltaItem) + } } + } else { + delta = evaluation.delta } const values = valuesPrevious.map((previousItemValue, index) => [ previousItemValue, previousItemValue + delta[index], ]) - const newDecomposition = { - ...decomposition, + newEvaluationByName[name] = { delta, + deltaAtVectorIndex: + vectorIndex < delta.length + ? delta[vectorIndex] + : evaluation === undefined + ? 0 + : evaluation.deltaAtVectorIndex, deltaIs0Array: delta.every((deltaItem) => deltaItem === 0), values, - valuesX0Is0Array: values.every(([x0]) => x0 === 0), + valuesAtVectorIndex: + vectorIndex < values.length + ? values[vectorIndex] + : evaluation === undefined + ? [0, 0] + : evaluation.valuesAtVectorIndex, } - if (vectorIndex < delta.length) { - newDecomposition.deltaAtVectorIndex = delta[vectorIndex] - } - if (vectorIndex < values.length) { - newDecomposition.valuesAtVectorIndex = values[vectorIndex] - } - newDecompositionByName[name] = newDecomposition - return newDecompositionByName + return newEvaluationByName } export function updateVectorIndex( - decompositionByName: DecompositionByName, + evaluationByName: EvaluationByName, vectorIndex: number, -): DecompositionByName { +): EvaluationByName { let changed = false - const newDecompositionByName: DecompositionByName = {} - for (const [name, decomposition] of Object.entries(decompositionByName)) { - let decompositionChanged = false - const newDecomposition = { ...decomposition } + const newEvaluationByName: EvaluationByName = {} + for (const [name, evaluation] of Object.entries(evaluationByName)) { + let evaluationChanged = false + const newEvaluation = { ...evaluation } const { delta, deltaAtVectorIndex, values, valuesAtVectorIndex } = - decomposition + evaluation if (vectorIndex < delta.length) { const newDeltaAtVectorIndex = delta[vectorIndex] if (newDeltaAtVectorIndex != deltaAtVectorIndex) { - newDecomposition.deltaAtVectorIndex = newDeltaAtVectorIndex - decompositionChanged = true + newEvaluation.deltaAtVectorIndex = newDeltaAtVectorIndex + evaluationChanged = true } } if (vectorIndex < values.length) { const newValuesAtVectorIndex = values[vectorIndex] if (newValuesAtVectorIndex != valuesAtVectorIndex) { - newDecomposition.valuesAtVectorIndex = newValuesAtVectorIndex - decompositionChanged = true + newEvaluation.valuesAtVectorIndex = newValuesAtVectorIndex + evaluationChanged = true } } - newDecompositionByName[name] = decompositionChanged - ? newDecomposition - : decomposition - if (decompositionChanged) { + newEvaluationByName[name] = evaluationChanged ? newEvaluation : evaluation + if (evaluationChanged) { changed = true } } - return changed ? newDecompositionByName : decompositionByName + return changed ? newEvaluationByName : evaluationByName } export function* walkDecompositions( diff --git a/src/lib/server/config.ts b/src/lib/server/config.ts index 63cf2751387b50b8b591e3a9f9dff92caaa995fb..2e25941ecc0bda657336212ee1740d4c23f459f5 100644 --- a/src/lib/server/config.ts +++ b/src/lib/server/config.ts @@ -4,8 +4,8 @@ import { validateConfig } from "$lib/auditors/config" export interface Config { advanced: boolean - apiBaseUrl: string - apiWebSocketBaseUrl: string + apiBaseUrls: string[] + apiWebSocketBaseUrls: string[] baseUrl: string childrenKey: string decompositionsPath: string @@ -42,7 +42,7 @@ export interface Config { const [validConfig, error] = validateConfig({ advanced: process.env["ADVANCED"], - apiBaseUrl: process.env["API_BASE_URL"], + apiBaseUrls: process.env["API_BASE_URLS"], baseUrl: process.env["BASE_URL"], childrenKey: process.env["CHILDREN_KEY"], decompositionsPath: process.env["DECOMPOSITION_PATH"], @@ -50,21 +50,24 @@ const [validConfig, error] = validateConfig({ githubPersonalAccessToken: process.env["GITHUB_PERSONAL_ACCESS_TOKEN"], hiddenEntitiesKeyPlural: process.env["HIDDEN_ENTITIES"], jsonDir: process.env["JSON_DIR"], - matomo: process.env["MATOMO_SITE_ID"] && process.env["MATOMO_URL"] ? { - prependDomain: process.env["MATOMO_PREPEND_DOMAIN"], - siteId: process.env["MATOMO_SITE_ID"], - subdomains: process.env["MATOMO_SUBDOMAINS"], - url: process.env["MATOMO_URL"], - } : null, + matomo: + process.env["MATOMO_SITE_ID"] && process.env["MATOMO_URL"] + ? { + prependDomain: process.env["MATOMO_PREPEND_DOMAIN"], + siteId: process.env["MATOMO_SITE_ID"], + subdomains: process.env["MATOMO_SUBDOMAINS"], + url: process.env["MATOMO_URL"], + } + : null, oauth2: process.env["OAUTH2_CLIENT_ID"] ? { - accessTokenUrl: process.env["OAUTH2_ACCESS_TOKEN_URL"], - authorizationUrl: process.env["OAUTH2_AUTHORIZATION_URL"], - clientId: process.env["OAUTH2_CLIENT_ID"], - clientSecret: process.env["OAUTH2_CLIENT_SECRET"], - jwtSecret: process.env["OAUTH2_JWT_SECRET"], - profileUrl: process.env["OAUTH2_PROFILE_URL"], - } + accessTokenUrl: process.env["OAUTH2_ACCESS_TOKEN_URL"], + authorizationUrl: process.env["OAUTH2_AUTHORIZATION_URL"], + clientId: process.env["OAUTH2_CLIENT_ID"], + clientSecret: process.env["OAUTH2_CLIENT_SECRET"], + jwtSecret: process.env["OAUTH2_JWT_SECRET"], + profileUrl: process.env["OAUTH2_PROFILE_URL"], + } : null, openfiscaRepository: { branch: process.env["OPENFISCA_BRANCH"], @@ -89,6 +92,8 @@ if (error !== null) { process.exit(-1) } const config = validConfig as Config -config.apiWebSocketBaseUrl = config.apiBaseUrl.replace(/^http/, "ws") +config.apiWebSocketBaseUrls = config.apiBaseUrls.map((url) => + url.replace(/^http/, "ws"), +) export default config diff --git a/src/lib/sessions.ts b/src/lib/sessions.ts index bdc73f7f3d71cdf2b12e2cfa87d8396056dbd1ea..09d2ec09f9574932faeb9e28c820365abdaf5728 100644 --- a/src/lib/sessions.ts +++ b/src/lib/sessions.ts @@ -6,8 +6,8 @@ import type { User } from "$lib/users" export interface Session { advanced: boolean - apiBaseUrl: string - apiWebSocketBaseUrl: string + apiBaseUrls: string[] + apiWebSocketBaseUrls: string[] baseUrl: string authenticationEnabled: boolean childrenKey: string diff --git a/src/lib/websockets.ts b/src/lib/websockets.ts new file mode 100644 index 0000000000000000000000000000000000000000..3210cdc1f78d2c458fdeafde22487f704ff713eb --- /dev/null +++ b/src/lib/websockets.ts @@ -0,0 +1,11 @@ +import type Sockette from "sockette" + +import type { CalculationName } from "$lib/decompositions" + +export type WebSocketByName = { + [name in CalculationName]: Sockette +} + +export type WebSocketOpenByName = { + [name in CalculationName]?: boolean +} diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte index 98056d929ab7c878fa6f874fbc1232dd37a6f258..1cef106b51a5a9f2d215c16e9823f4759d6ad1bc 100644 --- a/src/routes/__layout.svelte +++ b/src/routes/__layout.svelte @@ -2,26 +2,25 @@ import "../app.css" import type { EntityByKey } from "@openfisca/ast" - import Sockette from "sockette" import { setContext } from "svelte" import type { Writable } from "svelte/store" import { writable } from "svelte/store" + import Sockette from "sockette" import { browser } from "$app/env" import { page, session } from "$app/stores" import NavBar from "$lib/components/NavBar.svelte" + import type { EvaluationByName } from "$lib/decompositions" import { + calculationNames, decompositionByNameFromCore, walkDecompositionsCoreName, - updateDecompositionEvaluations, + updateEvaluations, } from "$lib/decompositions" import type { PopulationWithoutId, Situation } from "$lib/situations" import { getPopulationReservedKeys } from "$lib/situations" import type { VariableValues } from "$lib/variables" - - const deltaByNameArray: Writable<Array<{ [name: string]: number[] }>> = - writable(new Array($session.testCases.length).fill({})) - setContext("deltaByNameArray", deltaByNameArray) + import type { WebSocketByName, WebSocketOpenByName } from "$lib/websockets" const entityByKey = $session.entityByKey as EntityByKey @@ -33,21 +32,13 @@ const vectorLength = writable(1) setContext("vectorLength", vectorLength) - const decompositionByNameArray = writable( - $deltaByNameArray.map((deltaByName, situationIndex) => - updateDecompositionEvaluations( - decompositionByNameFromCore( - $session.decompositionCoreByName, - $session.rootDecompositionName, - ), - $session.rootDecompositionName, - deltaByName, - $vectorIndexes[situationIndex], - $vectorLength, - ), + const decompositionByName = writable( + decompositionByNameFromCore( + $session.decompositionCoreByName, + $session.rootDecompositionName, ), ) - setContext("decompositionByNameArray", decompositionByNameArray) + setContext("decompositionByName", decompositionByName) // Note: Duplicates are removed from decompositionsName, because a variable name // may appear more than once in decomposition. @@ -64,6 +55,29 @@ let entityKeyByVariableName: { [name: string]: string } = {} + const evaluationByNameArrayByCalculationName = writable( + Object.fromEntries( + calculationNames.map((calculationName) => [ + calculationName, + new Array($session.testCases.length) + .fill(null) + .map((_, situationIndex) => + updateEvaluations( + $decompositionByName, + {}, + $session.rootDecompositionName, + $vectorIndexes[situationIndex], + $vectorLength, + ), + ), + ]), + ), + ) + setContext( + "evaluationByNameArrayByCalculationName", + evaluationByNameArrayByCalculationName, + ) + const inputInstantsByVariableName: Writable<{ [name: string]: Set<string> }> = writable(extractInputInstantsFromTestCases($session.testCases)) @@ -90,11 +104,12 @@ let variableValuesByName: { [name: string]: VariableValues } = {} - const webSocket: Writable<Sockette | undefined> = writable(undefined) - setContext("webSocket", webSocket) + const webSocketByName: Writable<WebSocketByName | undefined> = + writable(undefined) + setContext("webSocketByName", webSocketByName) - const webSocketOpen = writable(false) - setContext("webSocketOpen", webSocketOpen) + const webSocketOpenByName: Writable<WebSocketOpenByName> = writable({}) + setContext("webSocketOpenByName", webSocketOpenByName) const year = writable(2021) setContext("year", year) @@ -222,106 +237,132 @@ } function openWebSocket() { - $webSocket = new Sockette( - new URL("simulations/calculate", $session.apiWebSocketBaseUrl).toString(), - { - // maxAttempts: 10, - onmessage: (event) => { - const result = JSON.parse(event.data) - if (result.errors !== undefined) { - console.error("API Error:", result) - } else if (result.token !== $calculationToken) { - console.log( - `Ignoring API response with invalid token: ${result.token} instead of ${$calculationToken}`, - ) - } else { - const entity = entityByKey[result.entity] - let testCasesPopulationCount = 0 - for (const situation of $testCases) { - const entitySituation = situation[entity.key_plural] ?? {} - const populationCount = Object.keys(entitySituation).length - testCasesPopulationCount += populationCount - } - - // Update decompositions. - if (decompositionsName.includes(result.name)) { - { - let populationIndex = 0 - $deltaByNameArray = $deltaByNameArray.map( - (deltaByName, situationIndex) => { - const situation = $testCases[situationIndex] - const entitySituation = situation[entity.key_plural] ?? {} - let sum = new Array($vectorLength).fill(0) - for (const [populationId, population] of Object.entries( - entitySituation, - ).sort(([populationId1], [populationId2]) => - populationId1.localeCompare(populationId2), - )) { - for ( - let index = populationIndex, sumIndex = 0; - sumIndex < $vectorLength; - index += testCasesPopulationCount, sumIndex++ - ) { - sum[sumIndex] += result.value[index] - } - populationIndex++ - } - return { - ...deltaByName, - [result.name]: $decompositionByNameArray[situationIndex][ - result.name - ].negate - ? sum.map((sumItem) => -sumItem) - : sum, - } - }, + let remainingApiWebSocketBaseUrls: string[] = [] + $webSocketByName = Object.fromEntries( + calculationNames.map((calculationName) => { + if (remainingApiWebSocketBaseUrls.length === 0) { + remainingApiWebSocketBaseUrls = [...$session.apiWebSocketBaseUrls] + } + const apiWebSocketBaseUrlIndex = Math.floor( + Math.random() * remainingApiWebSocketBaseUrls.length, + ) + const apiWebSocketBaseUrl = + remainingApiWebSocketBaseUrls[apiWebSocketBaseUrlIndex] + remainingApiWebSocketBaseUrls.splice(apiWebSocketBaseUrlIndex, 1) + const webSocket = new Sockette( + new URL("simulations/calculate", apiWebSocketBaseUrl).toString(), + { + // maxAttempts: 10, + onmessage: (event) => { + const result = JSON.parse(event.data) + if (result.errors !== undefined) { + console.error("API Error:", result) + } else if (result.token !== $calculationToken) { + console.log( + `Ignoring API response with invalid token: ${result.token} instead of ${$calculationToken}`, + ) + } else { + // Count total population of test cases. + const entity = entityByKey[result.entity] + let testCasesPopulationCount = 0 + for (const situation of $testCases) { + const entitySituation = situation[entity.key_plural] ?? {} + const populationCount = Object.keys(entitySituation).length + testCasesPopulationCount += populationCount + } + + // Update evaluations. + if (decompositionsName.includes(result.name)) { + // First, update delta of evalutions. + let populationIndex = 0 + const evaluationByNameArray = + $evaluationByNameArrayByCalculationName[ + calculationName + ].map( + (evaluationByName, situationIndex): EvaluationByName => { + const situation = $testCases[situationIndex] + const entitySituation = + situation[entity.key_plural] ?? {} + let sum = new Array($vectorLength).fill(0) + for (const [ + _populationId, + _population, + ] of Object.entries(entitySituation).sort( + ([populationId1], [populationId2]) => + populationId1.localeCompare(populationId2), + )) { + for ( + let index = populationIndex, sumIndex = 0; + sumIndex < $vectorLength; + index += testCasesPopulationCount, sumIndex++ + ) { + sum[sumIndex] += result.value[index] + } + populationIndex++ + } + return { + ...evaluationByName, + [result.name]: { + ...evaluationByName[result.name], + delta: $decompositionByName[result.name].negate + ? sum.map((sumItem) => -sumItem) + : sum, + }, + } + }, + ) + + // Then, update values of evaluations from their new delta. + $evaluationByNameArrayByCalculationName = { + ...$evaluationByNameArrayByCalculationName, + [calculationName]: evaluationByNameArray.map( + (evaluationByName, situationIndex) => + updateEvaluations( + $decompositionByName, + evaluationByName, + rootDecompositionName, + $vectorIndexes[situationIndex], + $vectorLength, + ), + ), + } + } + + // Update entity key & values of variables. + entityKeyByVariableName = { + ...entityKeyByVariableName, + [result.name]: result.entity, + } + variableValuesByName = { + ...variableValuesByName, + [result.name]: result.value, + } + + // Update situation. + $testCases = updateTestCasesVariableValues( + $testCases, + result.name, + $vectorIndexes, ) } - - $decompositionByNameArray = $decompositionByNameArray.map( - (decompositionByName, situationIndex) => - updateDecompositionEvaluations( - decompositionByName, - rootDecompositionName, - $deltaByNameArray[situationIndex], - $vectorIndexes[situationIndex], - $vectorLength, - ), - ) - } - - // Update entity key & values of variables. - entityKeyByVariableName = { - ...entityKeyByVariableName, - [result.name]: result.entity, - } - variableValuesByName = { - ...variableValuesByName, - [result.name]: result.value, - } - - // Update situation. - $testCases = updateTestCasesVariableValues( - $testCases, - result.name, - $vectorIndexes, - ) - } - }, - // onopen: (event) => console.log("[WebSocket] Connected!", event), - onopen: () => { - $webSocketOpen = true - $simulationRequested = true - }, - // onreconnect: (event) => - // console.log("[WebSocket] Reconnecting...", event), - // onmaximum: (event) => - // console.log("[WebSocket] Stop Attempting!", event), - // onclose: (event) => console.log("[WebSocket] Closed!", event), - // onerror: (event) => console.log("[WebSocket] Error:", event), - // timeout: 5e3,> - }, - ) + }, + // onopen: (event) => console.log("[WebSocket] Connected!", event), + onopen(this) { + $webSocketOpenByName[calculationName] = true + $simulationRequested = true + }, + // onreconnect: (event) => + // console.log("[WebSocket] Reconnecting...", event), + // onmaximum: (event) => + // console.log("[WebSocket] Stop Attempting!", event), + // onclose: (event) => console.log("[WebSocket] Closed!", event), + // onerror: (event) => console.log("[WebSocket] Error:", event), + // timeout: 5e3, + }, + ) + return [calculationName, webSocket] + }), + ) as unknown as WebSocketByName } function updateTestCasesFromVectorIndexes(vectorIndexes: number[]): void { diff --git a/src/routes/index.svelte b/src/routes/index.svelte index 5c8363b4e8fff74d6ef09bf8072328918bdd775e..d9c538cf42b4bd8d8d36b491ad31dd8d5c45eaf1 100644 --- a/src/routes/index.svelte +++ b/src/routes/index.svelte @@ -1,7 +1,6 @@ <script lang="ts"> import type { EntityByKey, GroupEntity } from "@openfisca/ast" import { getRolePersonsIdKey } from "@openfisca/ast" - import type Sockette from "sockette" import { getContext, setContext } from "svelte" import type { Writable } from "svelte/store" import { writable } from "svelte/store" @@ -17,11 +16,11 @@ import VariableReferredInputsPane from "$lib/components/variables/VariableReferredInputsPane.svelte" import StartTutorial from "$lib/components/tutorial/StartTutorial.svelte" import VariableReferredParametersPane from "$lib/components/variables/VariableReferredParametersPane.svelte" - import type { DecompositionByName } from "$lib/decompositions" - import { - updateDecompositionEvaluations, - updateVectorIndex, + import type { + DecompositionByName, + EvaluationByNameArrayByCalculationName, } from "$lib/decompositions" + import { updateEvaluations, updateVectorIndex } from "$lib/decompositions" import type { ReformChange } from "$lib/reforms" import type { Axis, @@ -31,6 +30,7 @@ } from "$lib/situations" import { getPopulationReservedKeys } from "$lib/situations" import type { SelfTargetAProps } from "$lib/urls" + import type { WebSocketByName, WebSocketOpenByName } from "$lib/websockets" type EditionMode = | { @@ -56,17 +56,16 @@ const adaptAmountsScale = writable(true) setContext("adaptAmountsScale", adaptAmountsScale) let axes: Axis[][] = [] + const billReformName: string | null = "PLF LFI" const date = new Date().toISOString().split("T")[0] - const decompositionByNameArray = getContext( - "decompositionByNameArray", - ) as Writable<DecompositionByName[]> + const decompositionByName = getContext( + "decompositionByName", + ) as Writable<DecompositionByName> const decompositionsName = getContext("decompositionsName") as string[] - const deltaByNameArray = getContext("deltaByNameArray") as Writable< - Array<{ - [name: string]: number[] - }> - > let editionMode: EditionMode = null + const evaluationByNameArrayByCalculationName = getContext( + "evaluationByNameArrayByCalculationName", + ) as Writable<EvaluationByNameArrayByCalculationName> const inputInstantsByVariableName = getContext( "inputInstantsByVariableName", ) as Writable<{ @@ -88,8 +87,12 @@ > const vectorIndexes = getContext("vectorIndexes") as Writable<number[]> const vectorLength = getContext("vectorLength") as Writable<number> - const webSocket = getContext("webSocket") as Writable<Sockette | undefined> - const webSocketOpen = getContext("webSocketOpen") as Writable<boolean> + const webSocketByName = getContext("webSocketByName") as Writable< + WebSocketByName | undefined + > + const webSocketOpenByName = getContext( + "webSocketOpenByName", + ) as Writable<WebSocketOpenByName> const year = getContext("year") as Writable<number> $: query = ensureValidQuery($page.query) @@ -121,17 +124,26 @@ $vectorLength = newVectorLength const situationIndex = $testCaseIndex - const newDecompositionByNameArray = [...$decompositionByNameArray] - newDecompositionByNameArray[situationIndex] = - updateDecompositionEvaluations( - newDecompositionByNameArray[situationIndex], - rootDecompositionName, - $deltaByNameArray[situationIndex], - $vectorIndexes[situationIndex], - newVectorLength, - ) - $decompositionByNameArray = newDecompositionByNameArray - if ($webSocketOpen) { + $evaluationByNameArrayByCalculationName = Object.fromEntries( + Object.entries($evaluationByNameArrayByCalculationName).map( + ([calculationName, evaluationByNameArray]) => { + evaluationByNameArray = [...evaluationByNameArray] + evaluationByNameArray[situationIndex] = updateEvaluations( + $decompositionByName, + evaluationByNameArray[situationIndex], + rootDecompositionName, + $vectorIndexes[situationIndex], + newVectorLength, + ) + return [calculationName, evaluationByNameArray] + }, + ), + ) + if ( + $webSocketOpenByName.amendment || + $webSocketOpenByName.bill || + $webSocketOpenByName.law + ) { submit() } } @@ -140,7 +152,11 @@ const situations = [...$testCases] situations[situationIndex] = situation $testCases = situations - // if ($webSocketOpen) { + // if ( + // $webSocketOpenByName.amendment || + // $webSocketOpenByName.bill || + // $webSocketOpenByName.law + // ) { // submit() // } } @@ -150,15 +166,27 @@ const newVectorIndexes = [...$vectorIndexes] newVectorIndexes[situationIndex] = detail.vectorIndex $vectorIndexes = newVectorIndexes - const decompositionByName = $decompositionByNameArray[situationIndex] - const newDecompositionByName = updateVectorIndex( - decompositionByName, - detail.vectorIndex, + let changed = false + const newEvaluationByNameArrayByCalculationName = Object.fromEntries( + Object.entries($evaluationByNameArrayByCalculationName).map( + ([calculationName, evaluationByNameArray]) => { + const evaluationByName = evaluationByNameArray[situationIndex] + const newEvaluationByName = updateVectorIndex( + evaluationByName, + detail.vectorIndex, + ) + if (newEvaluationByName !== evaluationByName) { + evaluationByNameArray = [...evaluationByNameArray] + evaluationByNameArray[situationIndex] = newEvaluationByName + changed = true + } + return [calculationName, evaluationByNameArray] + }, + ), ) - if (newDecompositionByName !== decompositionByName) { - const newDecompositionByNameArray = [...$decompositionByNameArray] - newDecompositionByNameArray[situationIndex] = newDecompositionByName - $decompositionByNameArray = newDecompositionByNameArray + if (changed) { + $evaluationByNameArrayByCalculationName = + newEvaluationByNameArrayByCalculationName } } @@ -468,21 +496,26 @@ } $calculationToken = uuidv4() - $webSocket.send( - JSON.stringify({ - // reform: "PLF LFI", - reform: Object.keys($reform).length > 0 ? $reform : undefined, - situation: aggregatedSituation, - period: $year.toString(), - token: $calculationToken, - variables: decompositionsName, - }), - ) - $webSocket.send( - JSON.stringify({ - calculate: true, - }), - ) + const message = { + calculate: true, + situation: aggregatedSituation, + period: $year.toString(), + token: $calculationToken, + variables: decompositionsName, + } + if ($webSocketOpenByName.law) { + $webSocketByName.law.send(JSON.stringify(message)) + } + if (billReformName !== null && $webSocketOpenByName.bill) { + $webSocketByName.bill.send( + JSON.stringify({ ...message, reform: billReformName }), + ) + } + if (Object.keys($reform).length > 0 && $webSocketOpenByName.amendment) { + $webSocketByName.amendment.send( + JSON.stringify({ ...message, reform: $reform }), + ) + } } </script>