engine-in-web-worker
Jérémy Rialland 2023-08-08 12:18:00 +02:00
parent 54bcc94227
commit 00bad59fbc
53 changed files with 2455 additions and 1310 deletions

View File

@ -84,6 +84,7 @@ module.exports = {
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/member-delimiter-style': [
'error',

View File

@ -3,7 +3,7 @@ import { join, resolve } from 'path'
import { defineConfig } from 'vite'
import { PageConfig } from '@/pages/simulateurs/_configs/types'
import type { PageConfig } from '@/pages/simulateurs/_configs/types'
import { objectTransform } from '../source/utils'

View File

@ -1,33 +1,23 @@
import { ErrorBoundary } from '@sentry/react'
import { FallbackRender } from '@sentry/react/types/errorboundary'
import rules from 'modele-social'
import { ComponentProps, StrictMode, useMemo } from 'react'
import { ComponentProps, StrictMode, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Route, Routes } from 'react-router-dom'
import { css, styled } from 'styled-components'
import Footer from '@/components/layout/Footer/Footer'
import Header from '@/components/layout/Header'
import {
engineFactory,
EngineProvider,
Rules,
useEngine,
useSetupSafeSituation,
} from '@/components/utils/EngineContext'
import { Container } from '@/design-system/layout'
import { useAxeCoreAnalysis } from '@/hooks/useAxeCoreAnalysis'
import { useGetFullURL } from '@/hooks/useGetFullURL'
import { useIsEmbedded } from '@/hooks/useIsEmbedded'
import { useLazyPromise } from '@/hooks/usePromise'
import { useSaveAndRestoreScrollPosition } from '@/hooks/useSaveAndRestoreScrollPosition'
import Landing from '@/pages/_landing/Landing'
import Page404 from '@/pages/404'
import Accessibilité from '@/pages/Accessibilité'
import Assistants from '@/pages/assistants/index'
import Budget from '@/pages/budget/index'
import IntegrationTest from '@/pages/dev/IntegrationTest'
import Documentation from '@/pages/Documentation'
import Iframes from '@/pages/iframes'
import Integration from '@/pages/integration/index'
import Nouveautés from '@/pages/nouveautés/index'
import Offline from '@/pages/Offline'
@ -36,53 +26,274 @@ import Simulateurs from '@/pages/simulateurs'
import SimulateursEtAssistants from '@/pages/simulateurs-et-assistants'
import Stats from '@/pages/statistiques/LazyStats'
import { useSitePaths } from '@/sitePaths'
import {
useAsyncGetRule,
useAsyncParsedRules,
useLazyPromiseOnSituationChange,
usePromiseOnSituationChange,
useShallowCopy,
useWorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import Provider, { ProviderProps } from './Provider'
import Redirections from './Redirections'
type RootProps = {
basename: ProviderProps['basename']
rulesPreTransform?: (rules: Rules) => Rules
// rulesPreTransform?: (rules: Rules) => Rules
}
const TestWorkerEngine = () => {
const [refresh, setRefresh] = useState(0)
const workerEngine = useWorkerEngine()
// const workerEngineCtx = useWorkerEngineContext()
const [, trigger] = useLazyPromise(
async () =>
workerEngine?.asyncSetSituationWithEngineId({
SMIC: '1000€/mois',
}),
[workerEngine],
{ defaultValue: 'loading...' }
)
const date = useAsyncGetRule('date', { defaultValue: 'loading...' })
const parsedRules = useAsyncParsedRules()
const resultSmic = usePromiseOnSituationChange(
() => workerEngine.asyncEvaluateWithEngineId('SMIC'),
[workerEngine],
{ defaultValue: 'loading...' }
)
const [resultLazySmic, triggerLazySmic] = useLazyPromiseOnSituationChange(
() => workerEngine.asyncEvaluateWithEngineId('SMIC'),
[workerEngine],
{ defaultValue: 'wait 2sec...' }
)
useEffect(() => {
console.log('??? useEffect')
void (async () => {
await workerEngine.isWorkerReady
setTimeout(() => {
void triggerLazySmic()
}, 3000)
})()
}, [triggerLazySmic, workerEngine.isWorkerReady])
const workerEngineCopy = useShallowCopy(workerEngine)
// // const workerEngineCopy = workerEngine
console.log('=========>', workerEngine, workerEngineCopy)
const [, triggerCopy] = useLazyPromise(async () => {
// console.log('+++++++++>', workerEngineCopy)
await workerEngineCopy?.asyncSetSituationWithEngineId({
SMIC: '2000€/mois',
})
}, [workerEngineCopy])
const dateCopy = useAsyncGetRule('date', {
defaultValue: 'loading...',
// workerEngine: workerEngineCopy,
})
const parsedRulesCopy = useAsyncParsedRules({
workerEngine: workerEngineCopy,
})
const resultSmicCopy = usePromiseOnSituationChange(
async () => workerEngineCopy?.asyncEvaluateWithEngineId('SMIC'),
[workerEngineCopy],
{
defaultValue: 'loading...',
workerEngine: workerEngineCopy,
}
)
const [resultLazySmicCopy, triggerLazySmicCopy] =
useLazyPromiseOnSituationChange(
async () => workerEngineCopy?.asyncEvaluateWithEngineId('SMIC'),
[workerEngineCopy],
{
defaultValue: 'wait 2sec...',
workerEngine: workerEngineCopy,
}
)
useEffect(() => {
// console.log('useEffect')
void (async () => {
await workerEngine.isWorkerReady
setTimeout(() => {
void triggerLazySmicCopy()
}, 3000)
})()
}, [triggerLazySmicCopy, workerEngine.isWorkerReady])
const { asyncSetSituationWithEngineId } = workerEngineCopy ?? {}
usePromiseOnSituationChange(async () => {
// console.log('**************>', workerEngineCopy, resultSmic)
if (
typeof resultSmic !== 'string' &&
typeof resultSmic.nodeValue === 'number'
) {
// console.log('ooooooooooooooooooo', resultSmic)
await asyncSetSituationWithEngineId?.({
SMIC: resultSmic.nodeValue + '€/mois',
})
}
}, [asyncSetSituationWithEngineId, resultSmic])
return (
<div>
<h1>Test worker engine</h1>
<button onClick={() => setRefresh((r) => r + 1)}>
Refresh {refresh}
</button>
<button onClick={() => void trigger()}>trigger</button>
<button onClick={() => void triggerCopy()}>trigger copy</button>
<p>
date title:{' '}
{JSON.stringify(typeof date === 'string' ? date : date?.title)}
</p>
<p>
parsedRules length:{' '}
{JSON.stringify(Object.entries(parsedRules ?? {}).length)}
</p>
<p>
resultSmic:{' '}
{JSON.stringify(
typeof resultSmic === 'string' ? resultSmic : resultSmic?.nodeValue
)}
</p>
<p>
resultLazySmic:{' '}
{JSON.stringify(
typeof resultLazySmic === 'string'
? resultLazySmic
: resultLazySmic?.nodeValue
)}
</p>
<p>workerEngineCopy: {JSON.stringify(workerEngineCopy?.engineId)}</p>
<p>
dateCopy title:{' '}
{JSON.stringify(
typeof dateCopy === 'string' ? dateCopy : dateCopy?.title
)}
</p>
<p>
parsedRulesCopy length:{' '}
{JSON.stringify(Object.entries(parsedRulesCopy ?? {}).length)}
</p>
<p>
resultSmicCopy:{' '}
{JSON.stringify(
typeof resultSmicCopy === 'string'
? resultSmicCopy
: resultSmicCopy?.nodeValue
)}
</p>
<p>
resultLazySmicCopy:{' '}
{JSON.stringify(
typeof resultLazySmicCopy === 'string'
? resultLazySmicCopy
: resultLazySmicCopy?.nodeValue
)}
</p>
</div>
)
}
export default function Root({
basename,
rulesPreTransform = (r) => r,
}: RootProps) {
const engine = useMemo(
() => engineFactory(rulesPreTransform(rules)),
}: // rulesPreTransform = (r) => r,
RootProps) {
// const situationVersion = useCreateWorkerEngine(basename)
// const engine = useMemo(
// () => engineFactory(rulesPreTransform(rules)),
// We need to keep [rules] in the dependency list for hot reload of the rules
// in dev mode, even if ESLint think it is unnecessary since `rules` isn't
// defined in the component scope.
//
// eslint-disable-next-line react-hooks/exhaustive-deps
[rules]
)
// // We need to keep [rules] in the dependency list for hot reload of the rules
// // in dev mode, even if ESLint think it is unnecessary since `rules` isn't
// // defined in the component scope.
// //
// // eslint-disable-next-line react-hooks/exhaustive-deps
// [rules]
// )
return (
<StrictMode>
<EngineProvider value={engine}>
<Provider basename={basename}>
<Redirections>
<Router />
</Redirections>
</Provider>
</EngineProvider>
{/* <EngineProvider value={engine}> */}
<Provider basename={basename}>
<Redirections>
<Router />
</Redirections>
</Provider>
{/* </EngineProvider> */}
</StrictMode>
)
}
const Router = () => {
const engine = useEngine()
/*
const exampleSyncValue = usePromiseOnSituationChange(
() => asyncEvaluate('SMIC'),
[]
)?.nodeValue
useSetupSafeSituation(engine)
const exampleSyncValueWithDefault = usePromiseOnSituationChange(
async () => (await asyncEvaluate('SMIC')).nodeValue,
[],
'loading...'
)
const [exampleAsyncValue, fireEvaluate] = useLazyPromise(
async (param: PublicodesExpression) =>
(await asyncEvaluate(param)).nodeValue,
[],
42
)
usePromise(async () => {
let count = 0
const interval = setInterval(() => {
void fireEvaluate(count++ % 2 === 0 ? 'date' : 'SMIC')
if (count === 7) clearInterval(interval)
}, 1000)
await new Promise((resolve) => setTimeout(resolve, 3000))
await asyncSetSituation({ date: '01/01/2022' })
await new Promise((resolve) => setTimeout(resolve, 3000))
await asyncSetSituation({ date: '01/01/2021' })
await new Promise((resolve) => setTimeout(resolve, 3000))
}, [fireEvaluate])
*/
return (
<Routes>
<Route path="/iframes/*" element={<Iframes />} />
<Route path="*" element={<App />} />
</Routes>
<>
{/* exemple sans valeur par defaut : {JSON.stringify(exampleSyncValue)}
<br />
exemple avec valeur par defaut :{' '}
{JSON.stringify(exampleSyncValueWithDefault)} <br />
exemple d'execution manuel : {JSON.stringify(exampleAsyncValue)} */}
{/* */}
<Routes>
<Route path="test-worker" element={<TestWorkerEngine />} />
{/* <Route path="/iframes/*" element={<Iframes />} /> */}
<Route path="*" element={<App />} />
</Routes>
</>
)
}
@ -96,11 +307,8 @@ const CatchOffline = ({ error }: ComponentProps<FallbackRender>) => {
const App = () => {
const { relativeSitePaths } = useSitePaths()
const { t } = useTranslation()
const fullURL = useGetFullURL()
useSaveAndRestoreScrollPosition()
const isEmbedded = useIsEmbedded()
if (!import.meta.env.PROD && import.meta.env.VITE_AXE_CORE_ENABLED) {
@ -108,7 +316,6 @@ const App = () => {
useAxeCoreAnalysis()
}
const documentationPath = useSitePaths().absoluteSitePaths.documentation.index
const engine = useEngine()
return (
<StyledLayout $isEmbedded={isEmbedded}>
@ -137,10 +344,10 @@ const App = () => {
<Routes>
<Route index element={<Landing />} />
<Route
{/* <Route
path={relativeSitePaths.assistants.index + '/*'}
element={<Assistants />}
/>
/> */}
<Route
path={relativeSitePaths.simulateurs.index + '/*'}
element={<Simulateurs />}
@ -149,15 +356,15 @@ const App = () => {
path={relativeSitePaths.simulateursEtAssistants + '/*'}
element={<SimulateursEtAssistants />}
/>
<Route
path={relativeSitePaths.documentation.index + '/*'}
element={
<Documentation
documentationPath={documentationPath}
engine={engine}
/>
}
/>
{/* <Route
path={relativeSitePaths.documentation.index + '/*'}
element={
<Documentation
documentationPath={documentationPath}
engine={engine}
/>
}
/> */}
<Route
path={relativeSitePaths.développeur.index + '/*'}
element={<Integration />}
@ -172,12 +379,10 @@ const App = () => {
path={relativeSitePaths.accessibilité}
element={<Accessibilité />}
/>
<Route
path="/dev/integration-test"
element={<IntegrationTest />}
/>
<Route path={relativeSitePaths.plan} element={<Plan />} />
<Route path="*" element={<Page404 />} />
@ -185,7 +390,6 @@ const App = () => {
</ErrorBoundary>
</Container>
</main>
{!isEmbedded && <Footer />}
</StyledLayout>
)

View File

@ -6,15 +6,21 @@ import { useDispatch, useSelector } from 'react-redux'
import { styled } from 'styled-components'
import { Switch } from '@/design-system/switch'
import { useLazyPromise } from '@/hooks/usePromise'
import { batchUpdateSituation } from '@/store/actions/actions'
import { situationSelector } from '@/store/selectors/simulationSelectors'
import { ReplaceReturnType } from '@/types/utils'
import { catchDivideByZeroError } from '@/utils'
import {
useAsyncGetRule,
usePromiseOnSituationChange,
useWorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import { ExplicableRule } from './conversation/Explicable'
import { Condition, WhenApplicable } from './EngineValue'
import { SimulationGoal } from './Simulation'
import { FromTop } from './ui/animate'
import { useEngine } from './utils/EngineContext'
const proportions = {
'entreprise . activités . revenus mixtes . proportions . service BIC':
@ -60,7 +66,12 @@ export default function ChiffreAffairesActivitéMixte({
<SimulationGoal
small
key={chiffreAffaires}
onUpdateSituation={adjustProportions}
onUpdateSituation={
adjustProportions as ReplaceReturnType<
ReturnType<typeof useAdjustProportions>,
void
>
}
dottedName={chiffreAffaires}
/>
))}
@ -72,11 +83,11 @@ export default function ChiffreAffairesActivitéMixte({
}
function useAdjustProportions(CADottedName: DottedName) {
const engine = useEngine()
const dispatch = useDispatch()
const workerEngine = useWorkerEngine()
return useCallback(
(name: DottedName, value?: PublicodesExpression) => {
const [, trigger] = useLazyPromise(
async (name: DottedName, value?: PublicodesExpression) => {
const checkValue = (
val: unknown
): val is { valeur: number; unité: string } =>
@ -87,61 +98,79 @@ function useAdjustProportions(CADottedName: DottedName) {
typeof val.valeur === 'number' &&
typeof val.unité === 'string'
const old = Object.values(proportions).map((chiffreAffaire) =>
serializeEvaluation(
engine.evaluate(
name === chiffreAffaire && checkValue(value)
? value
: chiffreAffaire
const old = await Promise.all(
Object.values(proportions).map(async (chiffreAffaire) =>
serializeEvaluation(
await workerEngine.asyncEvaluateWithEngineId(
name === chiffreAffaire && checkValue(value)
? value
: chiffreAffaire
)
)
)
)
const nouveauCA = serializeEvaluation(
engine.evaluate({ somme: old.filter(Boolean) })
await workerEngine.asyncEvaluateWithEngineId({
somme: old.filter(Boolean),
})
)
if (nouveauCA === '0€/an') {
return // Avoid division by 0
}
const situation = Object.entries(proportions).reduce(
(acc, [proportionName, valueName]) => {
const entries = Object.entries(proportions).map(
async ([proportionName, valueName]) => {
const newValue = serializeEvaluation(
engine.evaluate(
await workerEngine.asyncEvaluateWithEngineId(
valueName === name && checkValue(value)
? value
: { valeur: valueName, 'par défaut': '0€/an' }
)
)
const newProportion = serializeEvaluation(
catchDivideByZeroError(() =>
engine.evaluate({
await catchDivideByZeroError(() =>
workerEngine.asyncEvaluateWithEngineId({
valeur: `${newValue ?? ''} / ${nouveauCA ?? ''}`,
unité: '%',
})
)
)
return {
...acc,
[proportionName]: newProportion,
[valueName]: undefined,
}
},
return [proportionName, valueName, newProportion] as const
}
)
const situation = (await Promise.all(entries)).reduce(
(acc, [proportionName, valueName, newProportion]) => ({
...acc,
[proportionName]: newProportion,
[valueName]: undefined,
}),
{ [CADottedName]: nouveauCA }
)
dispatch(batchUpdateSituation(situation))
},
[CADottedName, engine, dispatch]
[CADottedName, dispatch, workerEngine]
)
return trigger
}
function ActivitéMixte() {
const dispatch = useDispatch()
const situation = useSelector(situationSelector)
const rule = useEngine().getRule('entreprise . activités . revenus mixtes')
const rule = useAsyncGetRule('entreprise . activités . revenus mixtes')
const workerEngine = useWorkerEngine()
const defaultChecked =
useEngine().evaluate('entreprise . activités . revenus mixtes')
.nodeValue === true
usePromiseOnSituationChange(
() =>
workerEngine.asyncEvaluateWithEngineId(
'entreprise . activités . revenus mixtes'
),
[workerEngine]
)?.nodeValue === true
const onMixteChecked = useCallback(
(checked: boolean) => {
dispatch(
@ -173,7 +202,7 @@ function ActivitéMixte() {
Activité mixte
</Switch>
</Trans>
<ExplicableRule dottedName={rule.dottedName} light />
{rule && <ExplicableRule dottedName={rule.dottedName} light />}
</StyledActivitéMixteContainer>
</div>
)

View File

@ -1,5 +1,5 @@
import { DottedName } from 'modele-social'
import Engine, {
import {
ASTNode,
EvaluatedNode,
formatValue,
@ -10,13 +10,21 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { keyframes, styled } from 'styled-components'
import {
useAsyncParsedRules,
usePromiseOnSituationChange,
useWorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import RuleLink from './RuleLink'
import { useEngine } from './utils/EngineContext'
// import { useEngine } from './utils/EngineContext'
export type ValueProps<Names extends string> = {
expression: PublicodesExpression
unit?: string
engine?: Engine<Names>
// engine?: Engine<Names>
engineId?: number
displayedUnit?: string
precision?: number
documentationPath?: string
@ -27,7 +35,7 @@ export type ValueProps<Names extends string> = {
export default function Value<Names extends string>({
expression,
unit,
engine,
engineId = 0,
displayedUnit,
flashOnChange = false,
precision,
@ -36,27 +44,41 @@ export default function Value<Names extends string>({
...props
}: ValueProps<Names>) {
const { language } = useTranslation().i18n
const workerEngine = useWorkerEngine()
if (expression === null) {
throw new TypeError('expression cannot be null')
}
const defaultEngine = useEngine()
const e = engine ?? defaultEngine
const isRule =
typeof expression === 'string' && expression in e.getParsedRules()
const evaluation = e.evaluate({
valeur: expression,
...(unit && { unité: unit }),
const parsedRules = useAsyncParsedRules({
workerEngine,
})
const isRule =
typeof expression === 'string' && parsedRules && expression in parsedRules
const evaluation = usePromiseOnSituationChange(
() =>
workerEngine.asyncEvaluateWithEngineId({
valeur: expression,
...(unit && { unité: unit }),
}),
[expression, unit, workerEngine]
)
const value = formatValue(evaluation, {
displayedUnit,
language,
precision,
}) as string
if (isRule && linkToRule) {
const ruleEvaluation = e.evaluate(expression)
const ruleEvaluation = usePromiseOnSituationChange(
async () =>
isRule &&
linkToRule &&
workerEngine.asyncEvaluateWithEngineId(expression),
[expression, isRule, linkToRule, workerEngine]
)
if (isRule && linkToRule && ruleEvaluation) {
let dottedName = expression as DottedName
if (ruleEvaluation.sourceMap?.mecanismName === 'replacement') {
dottedName =
@ -88,17 +110,16 @@ export default function Value<Names extends string>({
</StyledValue>
)
}
const flash = keyframes`
const flash = keyframes`
from {
background-color: white;
opacity: 0.8;
}
to {
background-color: transparent;
}
to {
background-color: transparent;
}
`
const StyledValue = styled.span<{ $flashOnChange: boolean }>`
@ -109,118 +130,168 @@ const StyledValue = styled.span<{ $flashOnChange: boolean }>`
type ConditionProps = {
expression: PublicodesExpression | ASTNode
children: React.ReactNode
engine?: Engine<DottedName>
// engine?: Engine<DottedName>
engineId?: number
}
export function Condition({
expression,
children,
engine: engineFromProps,
// engine: engineFromProps,
engineId = 0,
}: ConditionProps) {
const defaultEngine = useEngine()
const engine = engineFromProps ?? defaultEngine
const nodeValue = engine.evaluate({ '!=': [expression, 'non'] }).nodeValue
// const defaultEngine = useEngine()
// const engine = engineFromProps ?? defaultEngine
// const nodeValue = engine.evaluate({ '!=': [expression, 'non'] }).nodeValue
if (!nodeValue) {
return null
}
// if (!nodeValue) {
// return null
// }
return <>{children}</>
// return <>{children}</>
const workerEngine = useWorkerEngine()
const node = usePromiseOnSituationChange(
() =>
workerEngine.asyncEvaluateWithEngineId({
'!=': [expression, 'non'],
}),
[expression, workerEngine]
)
return !node?.nodeValue ? null : <>{children}</>
}
export function WhenValueEquals({
expression,
value,
children,
engine: engineFromProps,
// engine: engineFromProps,
engineId = 0,
}: ConditionProps & { value: string | number }) {
const defaultEngine = useEngine()
const engine = engineFromProps ?? defaultEngine
const nodeValue = engine.evaluate(expression).nodeValue
// const defaultEngine = useEngine()
// const engine = engineFromProps ?? defaultEngine
// const nodeValue = engine.evaluate(expression).nodeValue
if (nodeValue !== value) {
return null
}
// if (nodeValue !== value) {
// return null
// }
return <>{children}</>
// return <>{children}</>
const workerEngine = useWorkerEngine()
const node = usePromiseOnSituationChange(
() => workerEngine.asyncEvaluateWithEngineId(expression),
[expression, workerEngine]
)
return node?.nodeValue !== value ? null : <>{children}</>
}
export function WhenApplicable({
dottedName,
children,
engine,
engineId = 0,
}: {
dottedName: DottedName
children: React.ReactNode
engine?: Engine<DottedName>
// engine?: Engine<DottedName>
engineId?: number
}) {
const defaultEngine = useEngine()
const workerEngine = useWorkerEngine()
// const defaultEngine = useEngine()
const engineValue = engine ?? defaultEngine
// const engineValue = engine ?? defaultEngine
if (
engineValue.evaluate({ 'est applicable': dottedName }).nodeValue !== true
) {
return null
}
// if (
// engineValue.evaluate({ 'est applicable': dottedName }).nodeValue !== true
// ) {
// return null
// }
return <>{children}</>
// return <>{children}</>
const node = usePromiseOnSituationChange(
() =>
workerEngine.asyncEvaluateWithEngineId({
'est applicable': dottedName,
}),
[dottedName, workerEngine]
)
return node?.nodeValue !== true ? <>{children}</> : null
}
export function WhenNotApplicable({
dottedName,
children,
engine,
engineId = 0,
}: {
dottedName: DottedName
children: React.ReactNode
engine?: Engine<DottedName>
// engine?: Engine<DottedName>
engineId?: number
}) {
const defaultEngine = useEngine()
// const defaultEngine = useEngine()
const engineValue = engine ?? defaultEngine
// const engineValue = engine ?? defaultEngine
if (
engineValue.evaluate({ 'est non applicable': dottedName }).nodeValue !==
true
) {
return null
}
// if (
// engineValue.evaluate({ 'est non applicable': dottedName }).nodeValue !==
// true
// ) {
// return null
// }
return <>{children}</>
// return <>{children}</>
const workerEngine = useWorkerEngine()
const node = usePromiseOnSituationChange(
() =>
workerEngine.asyncEvaluateWithEngineId({
'est non applicable': dottedName,
}),
[dottedName, workerEngine]
)
return node?.nodeValue !== true ? null : <>{children}</>
}
export function WhenAlreadyDefined({
dottedName,
children,
engine,
engineId = 0,
}: {
dottedName: DottedName
children: React.ReactNode
engine?: Engine<DottedName>
// engine?: Engine<DottedName>
engineId?: number
}) {
const defaultEngine = useEngine()
const workerEngine = useWorkerEngine()
const node = usePromiseOnSituationChange(
() =>
workerEngine.asyncEvaluateWithEngineId({ 'est non défini': dottedName }),
[dottedName, workerEngine]
)
const engineValue = engine ?? defaultEngine
if (engineValue.evaluate({ 'est non défini': dottedName }).nodeValue) {
return null
}
return <>{children}</>
return node?.nodeValue ? null : <>{children}</>
}
export function WhenNotAlreadyDefined({
dottedName,
children,
engineId = 0,
}: {
dottedName: DottedName
children: React.ReactNode
engineId?: number
}) {
const engine = useEngine()
if (engine.evaluate({ 'est défini': dottedName }).nodeValue) {
return null
}
const workerEngine = useWorkerEngine()
const node = usePromiseOnSituationChange(
() => workerEngine.asyncEvaluateWithEngineId({ 'est défini': dottedName }),
[dottedName, workerEngine]
)
return <>{children}</>
return node?.nodeValue ? null : <>{children}</>
}

View File

@ -15,6 +15,15 @@ const FeedbackButton = ({ isEmbedded }: { isEmbedded?: boolean }) => {
const { t } = useTranslation()
const containerRef = useRef<HTMLElement | null>(null)
const [feedbackFormIsOpened, setFeedbackFormIsOpened] = useState(false)
// const { absoluteSitePaths } = useSitePaths()
// const currentPath = useLocation().pathname
// const isSimulateurSalaire =
// currentPath.includes(absoluteSitePaths.simulateurs.salarié) ||
// currentPath.includes(IFRAME_SIMULATEUR_EMBAUCHE_PATH)
// const { shouldShowRater, customTitle } = useFeedback()
useOnClickOutside(
containerRef,
() => !feedbackFormIsOpened && setIsFormOpen(false)

View File

@ -4,12 +4,18 @@ import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { styled } from 'styled-components'
import { useEngine, useInversionFail } from '@/components/utils/EngineContext'
// import { useEngine, useInversionFail } from '@/components/utils/EngineContext'
import { Message } from '@/design-system'
import { CloseButton } from '@/design-system/buttons'
import { Body } from '@/design-system/typography/paragraphs'
import { usePromise } from '@/hooks/usePromise'
import { hideNotification } from '@/store/actions/actions'
import { RootState } from '@/store/reducers/rootReducer'
import { isNotNull } from '@/utils'
import {
useWorkerEngine,
WorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import { ExplicableRule } from './conversation/Explicable'
import { Appear } from './ui/animate'
@ -26,44 +32,64 @@ type Notification = {
sévérité: 'avertissement' | 'information'
}
function getNotifications(engine: Engine) {
return Object.values(engine.getParsedRules())
.filter(
(rule) =>
rule.rawNode.type === 'notification' &&
!!engine.evaluate(rule.dottedName).nodeValue
async function getNotifications(workerEngine: WorkerEngine) {
return (
await Promise.all(
Object.values(await workerEngine.asyncGetParsedRulesWithEngineId()).map(
async (rule) =>
rule.rawNode.type === 'notification' &&
!!(await workerEngine.asyncEvaluateWithEngineId(rule.dottedName))
.nodeValue
? {
dottedName: rule.dottedName,
sévérité: rule.rawNode.sévérité,
résumé: rule.rawNode.résumé,
description: rule.rawNode.description,
}
: null
)
)
.map(({ dottedName, rawNode: { sévérité, résumé, description } }) => ({
dottedName,
sévérité,
résumé,
description,
}))
).filter(isNotNull)
// .map(({ dottedName, rawNode: { sévérité, résumé, description } }) => ({
// dottedName,
// sévérité,
// résumé,
// description,
// }))
}
export default function Notifications() {
const { t } = useTranslation()
const engine = useEngine()
const inversionFail = useInversionFail()
const workerEngine = useWorkerEngine()
// const inversionFail = useInversionFail()
const hiddenNotifications = useSelector(
(state: RootState) => state.simulation?.hiddenNotifications
)
const dispatch = useDispatch()
const messages: Array<Notification> = (
inversionFail
? [
{
dottedName: 'inversion fail',
description: t(
'simulateurs.inversionFail',
'Le montant saisi abouti à un résultat impossible. Cela est dû à un effet de seuil dans le calcul des cotisations.\n\nNous vous invitons à réessayer en modifiant légèrement le montant renseigné (quelques euros de plus par exemple).'
),
sévérité: 'avertissement',
} as Notification,
]
: (getNotifications(engine) as Array<Notification>)
).filter(({ dottedName }) => !hiddenNotifications?.includes(dottedName))
const messages = usePromise(
async () =>
(await getNotifications(workerEngine)).filter(
({ dottedName }) => !hiddenNotifications?.includes(dottedName)
),
[hiddenNotifications, workerEngine],
[]
)
// const messages: Array<Notification> = (
// inversionFail
// ? [
// {
// dottedName: 'inversion fail',
// description: t(
// 'simulateurs.inversionFail',
// 'Le montant saisi abouti à un résultat impossible. Cela est dû à un effet de seuil dans le calcul des cotisations.\n\nNous vous invitons à réessayer en modifiant légèrement le montant renseigné (quelques euros de plus par exemple).'
// ),
// sévérité: 'avertissement',
// } as Notification,
// ]
// : (getNotifications(engine) as Array<Notification>)
// )
// .filter(({ dottedName }) => !hiddenNotifications?.includes(dottedName))
const isMultiline = (str: string) => str.trim().split('\n').length > 1

View File

@ -17,6 +17,7 @@ import { H1, H4 } from '@/design-system/typography/heading'
import { Link } from '@/design-system/typography/link'
import { Body, Intro } from '@/design-system/typography/paragraphs'
import { EmbededContextProvider } from '@/hooks/useIsEmbedded'
import { WorkerEngineProvider } from '@/worker/socialWorkerEngineClient'
import { Message } from '../design-system'
import * as safeLocalStorage from '../storage/safeLocalStorage'
@ -27,7 +28,7 @@ import { IframeResizer } from './IframeResizer'
import { ServiceWorker } from './ServiceWorker'
import { DarkModeProvider } from './utils/DarkModeContext'
type SiteName = 'mon-entreprise' | 'infrance' | 'publicodes'
type SiteName = 'mon-entreprise' | 'infrance'
export const SiteNameContext = createContext<SiteName | null>(null)
@ -51,26 +52,28 @@ export default function Provider({
<I18nextProvider i18n={i18next}>
<ReduxProvider store={store}>
<BrowserRouterProvider basename={basename}>
<ErrorBoundary
fallback={(errorData) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<ErrorFallback {...errorData} showFeedbackForm />
)}
>
{!import.meta.env.SSR &&
import.meta.env.MODE === 'production' &&
'serviceWorker' in navigator && <ServiceWorker />}
<IframeResizer />
<OverlayProvider>
<ThemeColorsProvider>
<DisableAnimationOnPrintProvider>
<SiteNameContext.Provider value={basename}>
{children}
</SiteNameContext.Provider>
</DisableAnimationOnPrintProvider>
</ThemeColorsProvider>
</OverlayProvider>
</ErrorBoundary>
<WorkerEngineProvider basename={basename}>
<ErrorBoundary
fallback={(errorData) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<ErrorFallback {...errorData} />
)}
>
{!import.meta.env.SSR &&
import.meta.env.MODE === 'production' &&
'serviceWorker' in navigator && <ServiceWorker />}
<IframeResizer />
<OverlayProvider>
<ThemeColorsProvider>
<DisableAnimationOnPrintProvider>
<SiteNameContext.Provider value={basename}>
{children}
</SiteNameContext.Provider>
</DisableAnimationOnPrintProvider>
</ThemeColorsProvider>
</OverlayProvider>
</ErrorBoundary>
</WorkerEngineProvider>
</BrowserRouterProvider>
</ReduxProvider>
</I18nextProvider>

View File

@ -3,11 +3,12 @@ import { utils } from 'publicodes'
import { useContext } from 'react'
import { styled } from 'styled-components'
import { EngineContext, useEngine } from '@/components/utils/EngineContext'
// import { useEngine } from '@/components/utils/EngineContext'
import { Grid } from '@/design-system/layout'
import { Link } from '@/design-system/typography/link'
import { Li, Ul } from '@/design-system/typography/list'
import { capitalise0 } from '@/utils'
import { usePromise } from '@/hooks/usePromise'
import { capitalise0, isNotNullOrUndefined } from '@/utils'
export function References({
references,
@ -131,21 +132,37 @@ const getDomain = (link: string) =>
)
export function RuleReferences({ dottedNames }: { dottedNames: DottedName[] }) {
const engine = useContext(EngineContext)
const references = usePromise(
async () => {
const values = await Promise.all(
dottedNames.map(
async (dottedName) =>
(await asyncEvaluate(`${dottedName} != non`)).nodeValue
)
)
const refs = await Promise.all(
values
.filter(isNotNullOrUndefined)
.map(async (dottedName) =>
Object.entries(
(await asyncGetRule(dottedName as DottedName)).rawNode
.références ?? {}
)
)
)
return refs.flat()
},
[dottedNames],
[]
)
return (
<Ul>
{dottedNames
.filter(
(dottedName) => engine.evaluate(`${dottedName} != non`).nodeValue
)
.map((dottedName) =>
Object.entries(
engine.getRule(dottedName).rawNode.références ?? {}
).map(([title, href]) => (
<Reference key={href} title={title} href={href} />
))
)}
{references.map(([title, href]) => (
<Reference key={href} title={title} href={href} />
))}
</Ul>
)
}

View File

@ -1,12 +1,13 @@
import { DottedName } from 'modele-social'
import Engine from 'publicodes'
import { RuleLink as EngineRuleLink } from 'publicodes-react'
import React, { ReactNode, useContext } from 'react'
import React, { ReactNode } from 'react'
import { Link } from '@/design-system/typography/link'
import { useSitePaths } from '@/sitePaths'
import { EngineContext } from './utils/EngineContext'
import {
usePromiseOnSituationChange,
useWorkerEngine,
} from '@/worker/socialWorkerEngineClient'
// TODO : quicklink -> en cas de variations ou de somme avec un seul élément actif, faire un lien vers cet élément
export default function RuleLink(
@ -16,23 +17,32 @@ export default function RuleLink(
children?: React.ReactNode
documentationPath?: string
linkComponent?: ReactNode
engine?: Engine<DottedName>
engineId?: number
} & Omit<React.ComponentProps<typeof Link>, 'to' | 'children'>
) {
const engineId = props.engineId ?? 0
const { absoluteSitePaths } = useSitePaths()
const defaultEngine = useContext(EngineContext)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(false)
const workerEngine = useWorkerEngine()
const engineUsed = props?.engine ?? defaultEngine
usePromiseOnSituationChange(() => {
setLoading(true)
setError(false)
try {
engineUsed.getRule(props.dottedName)
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
return workerEngine
.asyncGetRuleWithEngineId(props.dottedName)
.catch(() => setError(true))
.then(() => setLoading(false))
}, [props.dottedName, workerEngine])
if (loading || error) {
return null
}
return <>EngineRuleLink</>
// TODO : publicodes-react ne supporte pas encore les engines dans un worker
return (
<EngineRuleLink
{...props}

View File

@ -1,13 +1,15 @@
import { Evaluation } from 'publicodes'
import { useContext } from 'react'
import { Trans } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { styled } from 'styled-components'
import Banner from '@/components/Banner'
import { EngineContext } from '@/components/utils/EngineContext'
import { Link as DesignSystemLink } from '@/design-system/typography/link'
import { updateSituation } from '@/store/actions/actions'
import {
usePromiseOnSituationChange,
useWorkerEngine,
} from '@/worker/socialWorkerEngineClient'
const Bold = styled.span<{ $bold: boolean }>`
${({ $bold }) => ($bold ? 'font-weight: bold;' : '')}
@ -15,16 +17,18 @@ const Bold = styled.span<{ $bold: boolean }>`
export const SelectSimulationYear = () => {
const dispatch = useDispatch()
const year = useContext(EngineContext).evaluate('date')
const workerEngine = useWorkerEngine()
const year = usePromiseOnSituationChange(
() => workerEngine.asyncEvaluateWithEngineId('date'),
[workerEngine]
)
const choices = [2022, 2023]
const actualYear = Number(
(year.nodeValue?.toString().slice(-4) as Evaluation<number> | undefined) ||
(year?.nodeValue?.toString().slice(-4) as Evaluation<number>) ||
new Date().getFullYear()
)
// return null // Waiting for next year.
return (
<Banner hideAfterFirstStep={false} icon={'📅'}>
<Trans i18nKey="pages.simulateurs.select-year.info">

View File

@ -34,9 +34,9 @@ export function useUrl() {
? import.meta.env.VITE_FR_BASE_URL
: import.meta.env.VITE_EN_BASE_URL
searchParams.set('utm_source', 'sharing')
searchParams?.set('utm_source', 'sharing')
return siteUrl + path + '?' + searchParams.toString()
return siteUrl + path + '?' + (searchParams ?? '').toString()
}
const ButtonLabel = styled.span`

View File

@ -1,5 +1,4 @@
import { Evaluation } from 'publicodes'
import { useContext } from 'react'
import { Trans } from 'react-i18next'
import { styled } from 'styled-components'
@ -8,8 +7,10 @@ import { Link } from '@/design-system/typography/link'
import { Li, Ul } from '@/design-system/typography/list'
import { Body } from '@/design-system/typography/paragraphs'
import { AbsoluteSitePaths } from '@/sitePaths'
import { EngineContext } from './utils/EngineContext'
import {
usePromiseOnSituationChange,
useWorkerEngine,
} from '@/worker/socialWorkerEngineClient'
type SimulateurWarningProps = {
simulateur: Exclude<keyof AbsoluteSitePaths['simulateurs'], 'index'>
@ -18,10 +19,13 @@ type SimulateurWarningProps = {
export default function SimulateurWarning({
simulateur,
}: SimulateurWarningProps) {
const year = useContext(EngineContext)
.evaluate('date')
.nodeValue?.toString()
.slice(-4) as Evaluation<number> | undefined
const workerEngine = useWorkerEngine()
const year = usePromiseOnSituationChange(
() => workerEngine.asyncEvaluateWithEngineId('date'),
[workerEngine]
)
?.nodeValue?.toString()
.slice(-4) as Evaluation<number>
return (
<Warning

View File

@ -10,13 +10,17 @@ import { Strong } from '@/design-system/typography'
import { Body, SmallBody } from '@/design-system/typography/paragraphs'
import { updateSituation } from '@/store/actions/actions'
import { targetUnitSelector } from '@/store/selectors/simulationSelectors'
import {
useAsyncGetRule,
usePromiseOnSituationChange,
useWorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import { ExplicableRule } from '../conversation/Explicable'
import RuleInput, { InputProps } from '../conversation/RuleInput'
import RuleLink from '../RuleLink'
import { Appear } from '../ui/animate'
import AnimatedTargetValue from '../ui/AnimatedTargetValue'
import { useEngine } from '../utils/EngineContext'
import { useInitialRender } from '../utils/useInitialRender'
type SimulationGoalProps = {
@ -48,14 +52,18 @@ export function SimulationGoal({
isInfoMode = false,
}: SimulationGoalProps) {
const dispatch = useDispatch()
const engine = useEngine()
const currentUnit = useSelector(targetUnitSelector)
const evaluation = engine.evaluate({
valeur: dottedName,
arrondi: round ? 'oui' : 'non',
...(!isTypeBoolean ? { unité: currentUnit } : {}),
})
const rule = engine.getRule(dottedName)
const workerEngine = useWorkerEngine()
const evaluation = usePromiseOnSituationChange(
() =>
workerEngine.asyncEvaluateWithEngineId({
value: dottedName,
arrondi: round ? 'oui' : 'non',
...(!isTypeBoolean ? { unité: currentUnit } : {}),
}),
[workerEngine, dottedName, round, isTypeBoolean, currentUnit]
)
const rule = useAsyncGetRule(dottedName)
const initialRender = useInitialRender()
const [isFocused, setFocused] = useState(false)
const onChange = useCallback(
@ -65,10 +73,11 @@ export function SimulationGoal({
},
[dispatch, onUpdateSituation, dottedName]
)
if (evaluation.nodeValue === null) {
return null
}
if (small && !editable && evaluation.nodeValue === undefined) {
if (
evaluation?.nodeValue === null ||
(small && !editable && evaluation?.nodeValue === undefined)
) {
return null
}
@ -96,7 +105,7 @@ export function SimulationGoal({
<StyledBody
id={`${dottedName.replace(/\s|\./g, '_')}-label`}
>
<Strong>{label || rule.title}</Strong>
<Strong>{label || rule?.title}</Strong>
</StyledBody>
</Grid>
<Grid item>
@ -114,7 +123,7 @@ export function SimulationGoal({
</RuleLink>
)}
{rule.rawNode.résumé && (
{rule?.rawNode.résumé && (
<StyledSmallBody
className={small ? 'sr-only' : ''}
id={`${dottedName.replace(/\s|\./g, '_')}-description`}
@ -129,7 +138,7 @@ export function SimulationGoal({
</StyledGuideLectureContainer>
{editable ? (
<Grid item md={small ? 2 : 3} sm={small ? 3 : 4} xs={4}>
{!isFocused && !small && (
{!isFocused && !small && evaluation && (
<AnimatedTargetValue value={evaluation.nodeValue as number} />
)}
<RuleInput
@ -140,7 +149,7 @@ export function SimulationGoal({
}
: undefined
}
aria-label={engine.getRule(dottedName)?.title}
aria-label={rule?.title}
aria-describedby={`${dottedName.replace(
/\s|\./g,
'_'
@ -151,7 +160,9 @@ export function SimulationGoal({
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onChange={onChange}
missing={dottedName in evaluation.missingVariables}
missing={
evaluation && dottedName in evaluation.missingVariables
}
small={small}
formatOptions={{
maximumFractionDigits: round ? 0 : 2,

View File

@ -9,9 +9,12 @@ import { styled } from 'styled-components'
import RuleLink from '@/components/RuleLink'
import useDisplayOnIntersecting from '@/components/utils/useDisplayOnIntersecting'
import { targetUnitSelector } from '@/store/selectors/simulationSelectors'
import {
usePromiseOnSituationChange,
useWorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import { DisableAnimationContext } from './utils/DisableAnimationContext'
import { useEngine } from './utils/EngineContext'
const BarStack = styled.div`
display: flex;
@ -139,6 +142,7 @@ export function StackedBarChart({
const styles = useSpring({ opacity: displayChart ? 1 : 0 })
return !useContext(DisableAnimationContext) ? (
// @ts-ignore type too deep
<animated.div ref={intersectionRef} style={styles}>
<InnerStackedBarChart data={data} precision={precision} />
</animated.div>
@ -201,20 +205,27 @@ export default function StackedRulesChart({
data,
precision = 0.1,
}: StackedRulesChartProps) {
const engine = useEngine()
const targetUnit = useSelector(targetUnitSelector)
const workerEngine = useWorkerEngine()
return (
<StackedBarChart
precision={precision}
data={data.map(({ dottedName, title, color }) => ({
key: dottedName,
value: engine.evaluate({ valeur: dottedName, unité: targetUnit })
.nodeValue,
legend: <RuleLink dottedName={dottedName}>{title}</RuleLink>,
title,
color,
}))}
/>
const datas = usePromiseOnSituationChange(
() =>
Promise.all(
data.map(async ({ dottedName, title, color }) => ({
key: dottedName,
value: (
await workerEngine.asyncEvaluateWithEngineId({
valeur: dottedName,
unité: targetUnit,
})
).nodeValue,
legend: <RuleLink dottedName={dottedName}>{title}</RuleLink>,
title,
color,
}))
),
[data, targetUnit, workerEngine]
)
return <StackedBarChart precision={precision} data={datas || []} />
}

View File

@ -1,11 +1,16 @@
import { DottedName } from 'modele-social'
import { PublicodesExpression, RuleNode, utils } from 'publicodes'
import {
EvaluatedNode,
PublicodesExpression,
RuleNode,
utils,
} from 'publicodes'
import { useCallback, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { styled } from 'styled-components'
import { EvaluatedRule, useEngine } from '@/components/utils/EngineContext'
import { EvaluatedRule } from '@/components/utils/EngineContext'
import { Message, PopoverWithTrigger } from '@/design-system'
import { Button } from '@/design-system/buttons'
import { Emoji } from '@/design-system/emoji'
@ -17,6 +22,7 @@ import { Link } from '@/design-system/typography/link'
import { Body, Intro } from '@/design-system/typography/paragraphs'
import { useCurrentSimulatorData } from '@/hooks/useCurrentSimulatorData'
import { useNextQuestions } from '@/hooks/useNextQuestion'
import { usePromise } from '@/hooks/usePromise'
import { answerQuestion, resetSimulation } from '@/store/actions/actions'
import { resetCompany } from '@/store/actions/companyActions'
import { isCompanyDottedName } from '@/store/reducers/companySituationReducer'
@ -25,7 +31,7 @@ import {
companySituationSelector,
situationSelector,
} from '@/store/selectors/simulationSelectors'
import { evaluateQuestion } from '@/utils'
import { useWorkerEngine } from '@/worker/socialWorkerEngineClient'
import Value from '../EngineValue'
import { JeDonneMonAvis } from '../JeDonneMonAvis'
@ -41,27 +47,43 @@ export default function AnswerList({ onClose, children }: AnswerListProps) {
const { t } = useTranslation()
const { currentSimulatorData } = useCurrentSimulatorData()
const dispatch = useDispatch()
const engine = useEngine()
const workerEngine = useWorkerEngine()
const situation = useSelector(situationSelector)
const companySituation = useSelector(companySituationSelector)
const passedQuestions = useSelector(answeredQuestionsSelector)
const answeredAndPassedQuestions = useMemo(
() =>
(Object.keys(situation) as DottedName[])
.filter(
(answered) => !passedQuestions.some((passed) => answered === passed)
const answeredAndPassedQuestions = usePromise(
async () =>
(
await Promise.all(
(Object.keys(situation) as DottedName[])
.filter(
(answered) =>
!passedQuestions.some((passed) => answered === passed)
)
.concat(passedQuestions)
.map(
async (dottedName) =>
await workerEngine.asyncGetRuleWithEngineId(dottedName)
)
)
.concat(passedQuestions)
.filter(
(dottedName) =>
engine.getRule(dottedName).rawNode.question !== undefined
)
.map((dottedName) => engine.getRule(dottedName)),
[engine, passedQuestions, situation]
).filter((rule) => rule.rawNode.question !== undefined),
[passedQuestions, situation, workerEngine],
[] as RuleNode<DottedName>[]
)
const nextQuestions = useNextQuestions()
const nextSteps = usePromise(
() =>
Promise.all(
nextQuestions.map(
async (dottedName) =>
workerEngine.asyncEvaluateWithEngineId(
await workerEngine.asyncGetRuleWithEngineId(dottedName)
) as Promise<EvaluatedNode>
)
),
[nextQuestions, workerEngine],
[] as EvaluatedRule[]
)
const nextSteps = useNextQuestions().map((dottedName) =>
engine.evaluate(engine.getRule(dottedName))
) as Array<EvaluatedRule>
const situationQuestions = useMemo(
() =>
@ -70,23 +92,33 @@ export default function AnswerList({ onClose, children }: AnswerListProps) {
),
[answeredAndPassedQuestions]
)
const companyQuestions = useMemo(
const companyQuestions = usePromise(
() =>
Array.from(
new Set(
(
[
...answeredAndPassedQuestions.map(({ dottedName }) => dottedName),
...Object.keys(situation),
...Object.keys(companySituation),
] as Array<DottedName>
).filter(isCompanyDottedName)
)
).map((dottedName) => engine.getRule(dottedName)),
[answeredAndPassedQuestions]
Promise.all(
Array.from(
new Set(
(
[
...answeredAndPassedQuestions.map(
({ dottedName }) => dottedName
),
...Object.keys(situation),
...Object.keys(companySituation),
] as Array<DottedName>
).filter(isCompanyDottedName)
)
).map((dottedName) => workerEngine.asyncGetRuleWithEngineId(dottedName))
),
[answeredAndPassedQuestions, companySituation, situation, workerEngine],
[] as RuleNode<DottedName>[]
)
const siret = engine.evaluate('établissement . SIRET').nodeValue as string
const siret = usePromise(
async () =>
(await workerEngine.asyncEvaluateWithEngineId('établissement . SIRET'))
.nodeValue as string,
[workerEngine]
)
return (
<div className="answer-list">
@ -101,7 +133,7 @@ export default function AnswerList({ onClose, children }: AnswerListProps) {
<Trans>Simulation en cours</Trans>
</H3>
<StepsTable {...{ rules: situationQuestions, onClose }} />
<StepsTable rules={situationQuestions} onClose={onClose} />
{children}
<div
className="print-hidden"
@ -203,7 +235,7 @@ export default function AnswerList({ onClose, children }: AnswerListProps) {
textAlign: 'center',
}}
></div>
<StepsTable {...{ rules: companyQuestions, onClose }} />
<StepsTable rules={companyQuestions} onClose={onClose} />
<Spacing md />
<div className="print-hidden">
<Body style={{ marginTop: 0 }}>
@ -231,7 +263,7 @@ export default function AnswerList({ onClose, children }: AnswerListProps) {
<Emoji emoji="🔮 " />
<Trans>Prochaines questions</Trans>
</H2>
<StepsTable {...{ rules: nextSteps, onClose }} />
<StepsTable rules={nextSteps} onClose={onClose} />
</div>
)}
</div>
@ -259,7 +291,7 @@ function StepsTable({
title: rule.title,
})}
light
dottedName={rule.dottedName}
dottedName={rule.dottedName as DottedName}
/>
</Grid>
<StyledAnswer item xs="auto">
@ -273,14 +305,19 @@ function StepsTable({
function AnswerElement(rule: RuleNode) {
const dispatch = useDispatch()
const engine = useEngine()
const workerEngine = useWorkerEngine()
const parentDottedName = utils.ruleParent(rule.dottedName) as DottedName
const questionDottedName = rule.rawNode.question
? (rule.dottedName as DottedName)
: parentDottedName && engine.getRule(parentDottedName).rawNode.API
? parentDottedName
: undefined
const questionDottedName = usePromise(
async () =>
rule.rawNode.question
? (rule.dottedName as DottedName)
: parentDottedName &&
(await workerEngine.asyncGetRuleWithEngineId(parentDottedName))
.rawNode.API
? parentDottedName
: undefined,
[parentDottedName, rule.dottedName, rule.rawNode.question, workerEngine]
)
const handleChange = useCallback(
(value: PublicodesExpression | undefined) => {
@ -311,7 +348,7 @@ function AnswerElement(rule: RuleNode) {
<>
<form onSubmit={onClose}>
<H3>
{evaluateQuestion(engine, engine.getRule(questionDottedName))}
{/* {evaluateQuestion(engine, engine.getRule(questionDottedName))} */}
<ExplicableRule light dottedName={questionDottedName} />
</H3>
<RuleInput

View File

@ -1,5 +1,5 @@
import { DottedName } from 'modele-social'
import Engine, { PublicodesExpression } from 'publicodes'
import Engine, { PublicodesExpression, RuleNode } from 'publicodes'
import React, { useCallback, useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
@ -7,7 +7,6 @@ import { useDispatch, useSelector } from 'react-redux'
import RuleInput from '@/components/conversation/RuleInput'
import Notifications from '@/components/Notifications'
import QuickLinks from '@/components/QuickLinks'
import { useEngine } from '@/components/utils/EngineContext'
import { Button } from '@/design-system/buttons'
import { Emoji } from '@/design-system/emoji'
import { Grid, Spacing } from '@/design-system/layout'
@ -19,7 +18,12 @@ import {
answeredQuestionsSelector,
situationSelector,
} from '@/store/selectors/simulationSelectors'
import { evaluateQuestion } from '@/utils'
import {
useAsyncGetRule,
usePromiseOnSituationChange,
useWorkerEngine,
WorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import { TrackPage } from '../ATInternetTracking'
import { JeDonneMonAvis } from '../JeDonneMonAvis'
@ -32,17 +36,16 @@ import { useNavigateQuestions } from './useNavigateQuestions'
export type ConversationProps = {
customEndMessages?: React.ReactNode
customSituationVisualisation?: React.ReactNode
engines?: Array<Engine<DottedName>>
workerEngines?: WorkerEngine[]
}
export default function Conversation({
customEndMessages,
customSituationVisualisation,
engines,
workerEngines,
}: ConversationProps) {
const { currentSimulatorData } = useCurrentSimulatorData()
const dispatch = useDispatch()
const engine = useEngine()
const situation = useSelector(situationSelector)
@ -55,7 +58,7 @@ export default function Conversation({
currentQuestionIsAnswered,
goToPrevious: goToPreviousQuestion,
goToNext: goToNextQuestion,
} = useNavigateQuestions(engines)
} = useNavigateQuestions(workerEngines)
const onChange = (
value: PublicodesExpression | undefined,
@ -88,6 +91,13 @@ export default function Conversation({
}, [focusFirstElemInForm, goToNextQuestion])
const formRef = React.useRef<HTMLFormElement>(null)
const workerEngine = useWorkerEngine()
const rule = useAsyncGetRule(currentQuestion)
const question = usePromiseOnSituationChange(
async () => rule && evaluateQuestion(workerEngine, rule),
[rule, workerEngine]
)
return (
<>
@ -120,7 +130,7 @@ export default function Conversation({
}}
>
<H3 id="questionHeader" as="h2">
{evaluateQuestion(engine, engine.getRule(currentQuestion))}
{question}
<ExplicableRule light dottedName={currentQuestion} />
</H3>
</div>
@ -241,3 +251,19 @@ export default function Conversation({
</>
)
}
export async function evaluateQuestion(
workerEngine: WorkerEngine,
rule: RuleNode
) {
const question = rule.rawNode.question
if (question && typeof question === 'object') {
return (
await workerEngine.asyncEvaluateWithEngineId(
question as PublicodesExpression
)
).nodeValue as string
}
return question
}

View File

@ -4,7 +4,7 @@ import { InputProps } from '@/components/conversation/RuleInput'
import { DateField } from '@/design-system/field'
import { DateFieldProps } from '@/design-system/field/DateField'
import { useEngine } from '../utils/EngineContext'
// import { useEngine } from '../utils/EngineContext'
import InputSuggestions from './InputSuggestions'
export default function DateInput({
@ -17,7 +17,7 @@ export default function DateInput({
value,
type,
}: InputProps & { type: DateFieldProps['type'] }) {
const engine = useEngine()
// const engine = useEngine()
const convertDate = (val?: unknown) => {
if (!val || typeof val !== 'string') {
@ -47,13 +47,12 @@ export default function DateInput({
<InputSuggestions
suggestions={suggestions}
onFirstClick={(node) => {
const value = engine.evaluate(node)
handleDateChange(
'nodeValue' in value && typeof value.nodeValue === 'string'
? value.nodeValue
: undefined
)
// const value = engine.evaluate(node)
// handleDateChange(
// 'nodeValue' in value && typeof value.nodeValue === 'string'
// ? value.nodeValue
// : undefined
// )
}}
onSecondClick={() => {
onSubmit?.('suggestion')

View File

@ -1,16 +1,18 @@
import { DottedName } from 'modele-social'
import { useContext } from 'react'
import { useContext, useEffect } from 'react'
import { EngineContext } from '@/components/utils/EngineContext'
// import { EngineContext } from '@/components/utils/EngineContext'
import { Markdown } from '@/components/utils/markdown'
import HelpButtonWithPopover from '@/design-system/buttons/HelpButtonWithPopover'
import { Spacing } from '@/design-system/layout'
import { H3 } from '@/design-system/typography/heading'
import { usePromise } from '@/hooks/usePromise'
import { useWorkerEngine } from '@/worker/socialWorkerEngineClient'
import { References } from '../References'
import RuleLink from '../RuleLink'
export function ExplicableRule<Names extends string = DottedName>({
export function ExplicableRule<Names extends DottedName>({
dottedName,
light,
bigPopover,
@ -22,15 +24,21 @@ export function ExplicableRule<Names extends string = DottedName>({
bigPopover?: boolean
title?: string
}) {
const engine = useContext(EngineContext)
const workerEngine = useWorkerEngine()
const rule = usePromise(
async () =>
dottedName != null
? workerEngine.asyncGetRuleWithEngineId(dottedName)
: null,
[dottedName, workerEngine]
)
// Rien à expliquer ici, ce n'est pas une règle
if (dottedName == null) {
return null
}
const rule = engine.getRule(dottedName)
if (rule.rawNode.description == null) {
if (rule?.rawNode.description == null) {
return null
}
@ -50,9 +58,7 @@ export function ExplicableRule<Names extends string = DottedName>({
>
<Markdown>{rule.rawNode.description}</Markdown>
<RuleLink dottedName={dottedName as DottedName}>
Lire la documentation
</RuleLink>
<RuleLink dottedName={dottedName}>Lire la documentation</RuleLink>
{rule.rawNode.références && (
<>

View File

@ -8,7 +8,7 @@ import { SmallBody } from '@/design-system/typography/paragraphs'
type InputSuggestionsProps = {
suggestions?: Record<string, ASTNode>
onFirstClick: (val: ASTNode) => void
onFirstClick: (val: ASTNode) => void | Promise<void>
onSecondClick?: (val: ASTNode) => void
className?: string
}
@ -32,7 +32,7 @@ export default function InputSuggestions({
<Link
key={text}
onPress={() => {
onFirstClick(value)
void onFirstClick(value)
if (suggestion !== value) {
setSuggestion(value)
} else {

View File

@ -5,35 +5,53 @@ import { useTranslation } from 'react-i18next'
import { Checkbox } from '@/design-system'
import { Emoji } from '@/design-system/emoji'
import { usePromise } from '@/hooks/usePromise'
import {
usePromiseOnSituationChange,
useWorkerEngine,
WorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import { ExplicableRule } from './Explicable'
import { InputProps } from './RuleInput'
import { InputProps, RuleWithMultiplePossibilities } from './RuleInput'
export function MultipleChoicesInput<Names extends string = DottedName>(
props: Omit<InputProps<Names>, 'onChange'> & {
choices: Array<RuleNode<Names>>
onChange: (value: PublicodesExpression, name: Names) => void
props: Omit<InputProps<DottedName>, 'onChange'> & {
engineId: number
onChange: (value: PublicodesExpression, name: DottedName) => void
}
) {
const handleChange = (isSelected: boolean, dottedName: Names) => {
const { engineId, dottedName, onChange } = props
const workerEngine = useWorkerEngine()
const choices = usePromise(
() => getMultiplePossibilitiesOptions(workerEngine, engineId, dottedName),
[dottedName, engineId, workerEngine],
[] as RuleNode<DottedName>[]
)
const handleChange = (isSelected: boolean, dottedName: DottedName) => {
// As soon as one option is selected, all the others are not missing anymore
return props.choices.forEach((choice) => {
const value =
dottedName === choice.dottedName
? isSelected
: props.engine.evaluate(choice).nodeValue
props.onChange(value ? 'oui' : 'non', choice.dottedName)
})
return Promise.all(
choices.map(async (choice) => {
const value =
dottedName === choice.dottedName
? isSelected
: (await workerEngine.asyncEvaluateWithEngineId(choice)).nodeValue
onChange(value ? 'oui' : 'non', choice.dottedName)
})
)
}
return (
<div aria-labelledby="questionHeader" role="group">
{props.choices.map((node) => (
{choices.map((node) => (
<Fragment key={node.dottedName}>
<CheckBoxRule
node={node}
onChange={(isSelected) => handleChange(isSelected, node.dottedName)}
engine={props.engine}
onChange={(isSelected) =>
void handleChange(isSelected, node.dottedName)
}
engineId={engineId}
/>
</Fragment>
))}
@ -43,11 +61,16 @@ export function MultipleChoicesInput<Names extends string = DottedName>(
type CheckBoxRuleProps = {
node: RuleNode
engine: Engine
engineId: number
onChange: (isSelected: boolean) => void
}
function CheckBoxRule({ node, engine, onChange }: CheckBoxRuleProps) {
const evaluation = engine.evaluate(node)
function CheckBoxRule({ node, engineId, onChange }: CheckBoxRuleProps) {
const workerEngine = useWorkerEngine()
const evaluation = usePromiseOnSituationChange(
() => workerEngine.asyncEvaluateWithEngineId(engineId, node),
[engineId, node, workerEngine]
)
const { t } = useTranslation()
if (evaluation.nodeValue === null) {
return null
@ -73,3 +96,32 @@ function CheckBoxRule({ node, engine, onChange }: CheckBoxRuleProps) {
</>
)
}
async function getMultiplePossibilitiesOptions(
workerEngine: WorkerEngine,
engineId: number,
// engine: Engine<Name>,
dottedName: DottedName
): Promise<RuleNode<DottedName>[]> {
// return (
// (engine.getRule(dottedName) as RuleWithMultiplePossibilities).rawNode[
// 'plusieurs possibilités'
// ] ?? []
// ).map((name) => engine.getRule(`${dottedName} . ${name}` as Name))
const posibilities =
(
(await workerEngine.asyncGetRuleWithEngineId(
engineId,
dottedName
)) as RuleWithMultiplePossibilities
).rawNode['plusieurs possibilités'] ?? []
return await Promise.all(
posibilities.map((name) =>
workerEngine.asyncGetRuleWithEngineId(
engineId,
`${dottedName} . ${name}` as DottedName
)
)
)
}

View File

@ -1,10 +1,10 @@
import { NumberFieldProps } from '@react-types/numberfield'
import { ASTNode, parseUnit, serializeUnit, Unit } from 'publicodes'
import { useCallback, useContext, useEffect, useState } from 'react'
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import { EngineContext } from '@/components/utils/EngineContext'
// import { EngineContext } from '@/components/utils/EngineContext'
import { NumberField } from '@/design-system/field'
import { debounce } from '@/utils'
@ -30,7 +30,7 @@ export default function NumberInput({
)
const { i18n, t } = useTranslation()
const parsedDisplayedUnit = displayedUnit ? parseUnit(displayedUnit) : unit
const engine = useContext(EngineContext)
useEffect(() => {
if (value !== currentValue) {
setCurrentValue(
@ -41,23 +41,27 @@ export default function NumberInput({
}
}, [value])
if (parsedDisplayedUnit && parsedDisplayedUnit.numerators.includes('€')) {
parsedDisplayedUnit.numerators = parsedDisplayedUnit.numerators.filter(
(u) => u === '€'
)
formatOptions = {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0,
...formatOptions,
}
} else {
formatOptions = {
const format = useMemo(() => {
let ret = {
style: 'decimal',
...formatOptions,
}
}
if (parsedDisplayedUnit && parsedDisplayedUnit.numerators.includes('€')) {
parsedDisplayedUnit.numerators = parsedDisplayedUnit.numerators.filter(
(u) => u === '€'
)
ret = {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0,
...formatOptions,
}
}
return ret
}, [formatOptions, parsedDisplayedUnit])
const debouncedOnChange = useCallback(debounce(1000, onChange), [])
return (
@ -83,7 +87,7 @@ export default function NumberInput({
debouncedOnChange(valeur)
}
}}
formatOptions={formatOptions}
formatOptions={format}
placeholder={
missing && value != null && typeof value === 'number'
? value
@ -94,8 +98,8 @@ export default function NumberInput({
<InputSuggestions
className="print-hidden"
suggestions={suggestions}
onFirstClick={(node: ASTNode) => {
const evaluatedNode = engine.evaluate(node)
onFirstClick={async (node: ASTNode) => {
const evaluatedNode = await asyncEvaluate(node)
if (serializeUnit(evaluatedNode.unit) === serializeUnit(unit)) {
setCurrentValue(evaluatedNode.nodeValue as number)
}

View File

@ -1,5 +1,5 @@
import { DottedName } from 'modele-social'
import Engine, {
import {
ASTNode,
EvaluatedNode,
Evaluation,
@ -7,13 +7,19 @@ import Engine, {
reduceAST,
RuleNode,
} from 'publicodes'
import React, { useContext } from 'react'
import React from 'react'
import NumberInput from '@/components/conversation/NumberInput'
import SelectCommune from '@/components/conversation/select/SelectCommune'
import { EngineContext } from '@/components/utils/EngineContext'
import { DateFieldProps } from '@/design-system/field/DateField'
import { getMeta } from '@/utils'
import { usePromise } from '@/hooks/usePromise'
import { getMeta, isNotNull } from '@/utils'
import {
useAsyncGetRule,
usePromiseOnSituationChange,
useWorkerEngine,
WorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import { Choice, MultipleAnswerInput, OuiNonInput } from './ChoicesInput'
import DateInput from './DateInput'
@ -49,7 +55,8 @@ type Props<Names extends string = DottedName> = Omit<
formatOptions?: Intl.NumberFormatOptions
displayedUnit?: string
modifiers?: Record<string, string>
engine?: Engine<DottedName>
// engine?: Engine<DottedName>
engineId?: number
}
export type InputProps<Name extends string = string> = Omit<
@ -61,7 +68,8 @@ export type InputProps<Name extends string = string> = Omit<
description: RuleNode['rawNode']['description']
value: EvaluatedNode['nodeValue']
onChange: (value: PublicodesExpression | undefined) => void
engine: Engine<Name>
// engine: Engine<Name>
engineId: number
}
export const binaryQuestion = [
@ -73,7 +81,7 @@ export const binaryQuestion = [
// be displayed to get a user input through successive if statements
// That's not great, but we won't invest more time until we have more diverse
// input components and a better type system.
export default function RuleInput<Names extends string = DottedName>({
export default function RuleInput({
dottedName,
onChange,
showSuggestions = true,
@ -81,24 +89,64 @@ export default function RuleInput<Names extends string = DottedName>({
showDefaultDateValue = false,
missing,
inputType,
modifiers = {},
engine,
modifiers,
engineId = 0,
...props
}: Props<Names>) {
const defaultEngine = useContext(EngineContext)
}: Props<DottedName>) {
// const defaultEngine = useContext(EngineContext)
const engineValue = (engine ?? defaultEngine) as Engine<Names>
// const engineValue = (engine ?? defaultEngine) as Engine<Names>
const rule = engineValue.getRule(dottedName)
const evaluation = engineValue.evaluate({ valeur: dottedName, ...modifiers })
const value = evaluation.nodeValue
const workerEngine = useWorkerEngine()
const commonProps: InputProps<Names> = {
const rule = useAsyncGetRule(dottedName)
// const evaluation = engineValue.evaluate({ valeur: dottedName, ...modifiers })
// async
const evaluation = usePromiseOnSituationChange(
() =>
workerEngine.asyncEvaluateWithEngineId({
valeur: dottedName,
...(modifiers ?? {}),
}),
[dottedName, modifiers, workerEngine]
)
const value = evaluation?.nodeValue
const isMultipleChoices = usePromiseOnSituationChange(
async () =>
rule && isMultiplePossibilities(workerEngine, engineId, dottedName),
[dottedName, engineId, rule, workerEngine]
)
console.log('=>', dottedName)
const choice = usePromise(
() => getOnePossibilityOptions(workerEngine, dottedName),
[workerEngine.situationVersion, dottedName]
)
dottedName === 'entreprise . activité . nature' &&
console.log(
'choice',
isMultipleChoices,
choice,
rule && isOnePossibility(rule)
)
if (!rule || isMultipleChoices === undefined) {
return <p>Chargement...</p>
}
const commonProps: InputProps<DottedName> = {
dottedName,
value,
missing:
missing ??
(!showDefaultDateValue && dottedName in evaluation.missingVariables),
(!showDefaultDateValue &&
evaluation &&
dottedName in evaluation.missingVariables),
onChange: (value: PublicodesExpression | undefined) =>
onChange(value, dottedName),
onSubmit,
@ -106,24 +154,25 @@ export default function RuleInput<Names extends string = DottedName>({
description: rule.rawNode.description,
question: rule.rawNode.question,
suggestions: showSuggestions ? rule.suggestions : {},
engine: engineValue,
// engine: engineValue,
engineId,
...props,
// Les espaces ne sont pas autorisés dans un id, les points sont assimilés à une déclaration de class CSS par Cypress
id: props?.id?.replace(/\s|\.]/g, '_') ?? dottedName.replace(/\s|\./g, '_'),
}
const meta = getMeta<{ affichage?: string }>(rule.rawNode, {})
if (isMultiplePossibilities(engineValue, dottedName)) {
if (isMultipleChoices) {
return (
<MultipleChoicesInput
{...commonProps}
choices={getMultiplePossibilitiesOptions(engineValue, dottedName)}
engineId={engineId}
onChange={onChange}
/>
)
}
if (isOnePossibility(engineValue.getRule(dottedName))) {
if (isOnePossibility(rule) && choice) {
const type =
inputType ??
(meta.affichage &&
@ -131,32 +180,22 @@ export default function RuleInput<Names extends string = DottedName>({
? (meta.affichage as 'radio' | 'card' | 'toggle' | 'select')
: 'radio')
return (
<MultipleAnswerInput
{...commonProps}
choice={getOnePossibilityOptions(engineValue, dottedName)}
type={type}
/>
)
return <MultipleAnswerInput {...commonProps} choice={choice} type={type} />
}
if (rule.rawNode.API && rule.rawNode.API === 'commune') {
return (
<SelectCommune
return `<SelectCommune
{...commonProps}
onChange={(c) => commonProps.onChange({ batchUpdate: c })}
value={value as Evaluation<string>}
/>
)
/>`
}
if (rule.rawNode.API && rule.rawNode.API.startsWith('pays détachement')) {
return (
<SelectPaysDétachement
return `<SelectPaysDétachement
{...commonProps}
plusFrance={rule.rawNode.API.endsWith('plus France')}
/>
)
/>`
}
if (rule.rawNode.API) {
throw new Error(
@ -165,39 +204,33 @@ export default function RuleInput<Names extends string = DottedName>({
}
if (rule.dottedName === 'établissement . taux ATMP . taux collectif') {
return <SelectAtmp {...commonProps} />
return '<SelectAtmp {...commonProps} />'
}
if (rule.rawNode.type?.startsWith('date')) {
return (
<DateInput
return `<DateInput
{...commonProps}
type={rule.rawNode.type as DateFieldProps['type']}
/>
)
/>`
}
if (
evaluation.unit == null &&
evaluation?.unit == null &&
['booléen', 'notification', undefined].includes(rule.rawNode.type) &&
typeof evaluation.nodeValue !== 'number'
typeof evaluation?.nodeValue !== 'number'
) {
return <OuiNonInput {...commonProps} />
}
if (rule.rawNode.type === 'texte') {
return (
<TextInput
return `<TextInput
{...commonProps}
label={undefined}
value={value as Evaluation<string>}
/>
)
/>`
}
if (rule.rawNode.type === 'paragraphe') {
return (
<ParagrapheInput {...commonProps} value={value as Evaluation<string>} />
)
return '<ParagrapheInput {...commonProps} value={value as Evaluation<string>} />'
}
// Pas de title sur NumberInput pour avoir une bonne expérience avec
@ -207,7 +240,7 @@ export default function RuleInput<Names extends string = DottedName>({
return (
<NumberInput
{...commonProps}
unit={evaluation.unit}
unit={evaluation?.unit}
value={value as Evaluation<number>}
/>
)
@ -224,11 +257,15 @@ const isOnePossibility = (node: RuleNode) =>
node
)
export const getOnePossibilityOptions = <Name extends string>(
engine: Engine<Name>,
path: Name
): Choice => {
const node = engine.getRule(path)
const getOnePossibilityOptions = async (
workerEngine: WorkerEngine,
// engineId: number,
path: DottedName
): Promise<Choice> => {
const node = await workerEngine.asyncGetRuleWithEngineId(path)
// if (path === 'entreprise . activité . nature') debugger
if (!node) {
throw new Error(`La règle ${path} est introuvable`)
}
@ -237,47 +274,58 @@ export const getOnePossibilityOptions = <Name extends string>(
variant &&
(!variant['choix obligatoire'] || variant['choix obligatoire'] === 'non')
return Object.assign(
const ttt = Object.assign(
node,
variant
? {
canGiveUp,
children: (
variant.explanation as (ASTNode & {
nodeKind: 'reference'
})[]
)
.filter(
(explanation) => engine.evaluate(explanation).nodeValue !== null
await Promise.all(
(
variant.explanation as (ASTNode & { nodeKind: 'reference' })[]
).map(async (explanation) => {
console.log('=>>>>', explanation)
const evaluate = await workerEngine.asyncEvaluateWithEngineId(
explanation
)
return evaluate.nodeValue !== null
? await getOnePossibilityOptions(
workerEngine,
explanation.dottedName as DottedName
)
: null
})
)
.map(({ dottedName }) =>
getOnePossibilityOptions(engine, dottedName as Name)
),
).filter(isNotNull),
}
: null
) as Choice
console.log('choice=>', ttt)
return ttt
}
type RuleWithMultiplePossibilities = RuleNode & {
export type RuleWithMultiplePossibilities = RuleNode & {
rawNode: RuleNode['rawNode'] & {
'plusieurs possibilités'?: Array<string>
}
}
function isMultiplePossibilities<Name extends string>(
engine: Engine<Name>,
dottedName: Name
): boolean {
return !!(engine.getRule(dottedName) as RuleWithMultiplePossibilities)
.rawNode['plusieurs possibilités']
}
function getMultiplePossibilitiesOptions<Name extends string>(
engine: Engine<Name>,
dottedName: Name
): RuleNode<Name>[] {
return (
(engine.getRule(dottedName) as RuleWithMultiplePossibilities).rawNode[
'plusieurs possibilités'
] ?? []
).map((name) => engine.getRule(`${dottedName} . ${name}` as Name))
async function isMultiplePossibilities(
workerEngine: WorkerEngine,
engineId: number,
// Engine<Name>,
dottedName: DottedName
): Promise<boolean> {
// return !!(engine.getRule(dottedName) as RuleWithMultiplePossibilities)
// .rawNode['plusieurs possibilités']
return !!(
(await workerEngine.asyncGetRuleWithEngineId(
dottedName
)) as RuleWithMultiplePossibilities
).rawNode['plusieurs possibilités']
}

View File

@ -1,9 +1,6 @@
import { DottedName } from 'modele-social'
import Engine from 'publicodes'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useEngine } from '@/components/utils/EngineContext'
import { useNextQuestions } from '@/hooks/useNextQuestion'
import {
goToQuestion,
@ -15,14 +12,17 @@ import {
currentQuestionSelector,
useMissingVariables,
} from '@/store/selectors/simulationSelectors'
import {
useWorkerEngine,
WorkerEngine,
} from '@/worker/socialWorkerEngineClient'
export function useNavigateQuestions(engines?: Array<Engine<DottedName>>) {
export function useNavigateQuestions(workerEngines?: WorkerEngine[]) {
const dispatch = useDispatch()
const engine = useEngine()
const nextQuestion = useNextQuestions(engines)[0]
const nextQuestions = useNextQuestions(workerEngines)
const currentQuestion = useSelector(currentQuestionSelector)
const missingVariables = useMissingVariables({ engines: engines ?? [engine] })
const missingVariables = useMissingVariables(workerEngines)
const currentQuestionIsAnswered =
currentQuestion && !(currentQuestion in missingVariables)
@ -40,13 +40,13 @@ export function useNavigateQuestions(engines?: Array<Engine<DottedName>>) {
}
useEffect(() => {
if (!currentQuestion && nextQuestion) {
dispatch(goToQuestion(nextQuestion))
if (!currentQuestion && nextQuestions[0]) {
dispatch(goToQuestion(nextQuestions[0]))
}
}, [nextQuestion, currentQuestion])
}, [nextQuestions, currentQuestion, dispatch])
return {
currentQuestion: currentQuestion ?? nextQuestion,
currentQuestion: currentQuestion ?? nextQuestions[0],
currentQuestionIsAnswered,
goToPrevious,
goToNext,

View File

@ -13,7 +13,6 @@ import Value, {
} from '@/components/EngineValue'
import RuleLink from '@/components/RuleLink'
import { FromBottom } from '@/components/ui/animate'
import { useEngine } from '@/components/utils/EngineContext'
import { Message } from '@/design-system'
import { Emoji } from '@/design-system/emoji'
import { Grid } from '@/design-system/layout'
@ -171,25 +170,36 @@ export function ImpôtsDGFIP({ role }: { role?: string }) {
)
}
const caisses = [
'CARCDSF',
'CARPIMKO',
'CIPAV',
'CARMF',
'CNBF',
'CAVEC',
'CAVP',
] as const
function CaisseRetraite({ role }: { role?: string }) {
const engine = useEngine()
const unit = useSelector(targetUnitSelector)
const caisses = [
'CARCDSF',
'CARPIMKO',
'CIPAV',
'CARMF',
'CNBF',
'CAVEC',
'CAVP',
] as const
const rules = usePromiseOnSituationChange(
() =>
Promise.all(
caisses.map((caisse) =>
asyncGetRule(
`dirigeant . indépendant . PL . ${caisse} . cotisations` as DottedName
)
)
),
[]
)
return (
<>
{caisses.map((caisse) => {
{caisses.map((caisse, index) => {
const dottedName =
`dirigeant . indépendant . PL . ${caisse}` as DottedName
const { description, références } = engine.getRule(dottedName).rawNode
const { description, références } = rules?.[index].rawNode || {}
return (
<Condition expression={dottedName} key={caisse}>
@ -243,9 +253,9 @@ function CaisseRetraite({ role }: { role?: string }) {
export function InstitutionsPartenairesArtisteAuteur() {
const unit = useSelector(targetUnitSelector)
const { description: descriptionIRCEC } = useEngine().getRule(
'artiste-auteur . cotisations . IRCEC'
).rawNode
const descriptionIRCEC = useAsyncGetRule(
'artiste-auteur . cotisations . IRCEC' as DottedName
)?.rawNode.description
return (
<section>

View File

@ -6,7 +6,7 @@ import Engine, {
Rule,
RuleNode,
} from 'publicodes'
import { createContext, useContext, useMemo } from 'react'
import { createContext, useContext, useEffect, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { deleteFromSituation } from '@/store/actions/actions'
@ -16,6 +16,10 @@ import {
situationSelector,
} from '@/store/selectors/simulationSelectors'
import { omit } from '@/utils'
import {
useWorkerEngine,
WorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import i18n from '../../locales/i18n'
@ -63,12 +67,12 @@ export function engineFactory(rules: Rules, options = {}) {
return new Engine(rules, { ...engineOptions, ...options, logger })
}
export const EngineContext = createContext<Engine>(new Engine())
export const EngineProvider = EngineContext.Provider
// export const EngineContext = createContext<Engine>(new Engine())
// export const EngineProvider = EngineContext.Provider
export function useEngine() {
return useContext(EngineContext) as Engine<DottedName>
}
// export function useEngine() {
// return useContext(EngineContext) as Engine<DottedName>
// }
export const useRawSituation = () => {
const simulatorSituation = useSelector(situationSelector)
@ -97,6 +101,8 @@ export const safeSetSituation = <Names extends string>(
situation: Partial<Record<Names, PublicodesExpression>>
faultyDottedName?: Names
}) => void
// rawSituation: Parameters<Engine<Names>['setSituation']>[0],
// options: Parameters<Engine<Names>['setSituation']>[1]
) => {
let situationError = false
const errors: Error[] = []
@ -104,6 +110,7 @@ export const safeSetSituation = <Names extends string>(
do {
try {
// Try to set situation
// engine.setSituation(situation, options)
engine.setSituation(situation)
situationError = false
} catch (error) {
@ -148,46 +155,58 @@ export const safeSetSituation = <Names extends string>(
} while (situationError && errors.length < 1000)
}
export const useSetupSafeSituation = (engine: Engine<DottedName>) => {
// engine: Engine<DottedName>
export const useSetupSafeSituation = (workerEngine?: WorkerEngine) => {
const dispatch = useDispatch()
// const workerEngine = useWorkerEngine()
const rawSituation = useRawSituation()
const simulatorSituation = useSelector(situationSelector)
const configSituation = useSelector(configSituationSelector)
const companySituation = useSelector(companySituationSelector)
const { asyncSetSituationWithEngineId } = workerEngine ?? {}
try {
safeSetSituation(engine, rawSituation, ({ faultyDottedName }) => {
if (!faultyDottedName) {
throw new Error('Bad empty faultyDottedName')
}
useEffect(() => {
if (!asyncSetSituationWithEngineId) {
return
}
console.log('set rawSituation', rawSituation, workerEngine)
if (faultyDottedName in simulatorSituation) {
dispatch(deleteFromSituation(faultyDottedName))
} else {
throw new Error(
'Bad ' +
(faultyDottedName in configSituation
? 'config'
: faultyDottedName in companySituation
? 'company'
: 'unknow') +
' situation : ' +
JSON.stringify(faultyDottedName)
)
}
})
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
void asyncSetSituationWithEngineId(rawSituation)
}, [asyncSetSituationWithEngineId, rawSituation])
engine.setSituation()
}
// try {
// safeSetSituation(engine, rawSituation, ({ faultyDottedName }) => {
// if (!faultyDottedName) {
// throw new Error('Bad empty faultyDottedName')
// }
// if (faultyDottedName in simulatorSituation) {
// dispatch(deleteFromSituation(faultyDottedName))
// } else {
// throw new Error(
// 'Bad ' +
// (faultyDottedName in configSituation
// ? 'config'
// : faultyDottedName in companySituation
// ? 'company'
// : 'unknow') +
// ' situation : ' +
// JSON.stringify(faultyDottedName)
// )
// }
// })
// } catch (error) {
// // eslint-disable-next-line no-console
// console.error(error)
// engine.setSituation()
// }
}
export function useInversionFail() {
return useContext(EngineContext).inversionFail()
}
// export function useInversionFail() {
// return useContext(EngineContext).inversionFail()
// }
export type EvaluatedRule = EvaluatedNode &
RuleNode & { dottedName: DottedName }

View File

@ -1,13 +1,19 @@
import { DottedName } from 'modele-social'
import Engine, { ParsedRules, serializeEvaluation } from 'publicodes'
import { ParsedRules, serializeEvaluation } from 'publicodes'
import { useEffect, useMemo, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useSearchParams } from 'react-router-dom'
import { useEngine } from '@/components/utils/EngineContext'
// import { useEngine } from '@/components/utils/EngineContext'
import { batchUpdateSituation, setActiveTarget } from '@/store/actions/actions'
import { Situation } from '@/store/reducers/rootReducer'
import { configObjectifsSelector } from '@/store/selectors/simulationSelectors'
import {
useAsyncParsedRules,
usePromiseOnSituationChange,
useWorkerEngine,
WorkerEngine,
} from '@/worker/socialWorkerEngineClient'
type ShortName = string
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
@ -18,11 +24,11 @@ export default function useSearchParamsSimulationSharing() {
const [searchParams, setSearchParams] = useSearchParams()
const objectifs = useSelector(configObjectifsSelector)
const dispatch = useDispatch()
const engine = useEngine()
const parsedRules = useAsyncParsedRules()
const dottedNameParamName = useMemo(
() => getRulesParamNames(engine.getParsedRules()),
[engine]
() => (parsedRules ? getRulesParamNames(parsedRules) : []),
[parsedRules]
)
useEffect(() => {
@ -64,13 +70,25 @@ export default function useSearchParamsSimulationSharing() {
}
export const useParamsFromSituation = (situation: Situation) => {
const engine = useEngine()
const parsedRules = useAsyncParsedRules()
const workerEngine = useWorkerEngine()
const dottedNameParamName = useMemo(
() => getRulesParamNames(engine.getParsedRules()),
[engine]
() => (parsedRules ? getRulesParamNames(parsedRules) : []),
[parsedRules]
)
return getSearchParamsFromSituation(engine, situation, dottedNameParamName)
const ret = usePromiseOnSituationChange(
() =>
getSearchParamsFromSituation(
workerEngine,
situation,
dottedNameParamName
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[dottedNameParamName, workerEngine]
)
return ret
}
export const cleanSearchParams = (
@ -99,29 +117,36 @@ export const getRulesParamNames = (
ruleNode.rawNode['identifiant court'] || dottedName,
])
export function getSearchParamsFromSituation(
engine: Engine,
export async function getSearchParamsFromSituation(
workerEngine: WorkerEngine,
situation: Situation,
dottedNameParamName: [DottedName, ParamName][]
): URLSearchParams {
): Promise<URLSearchParams> {
const searchParams = new URLSearchParams()
const dottedNameParamNameMapping = Object.fromEntries(dottedNameParamName)
Object.entries(situation).forEach(([dottedName, value]) => {
const paramName = dottedNameParamNameMapping[dottedName]
try {
const serializedValue = serializeEvaluation(engine.evaluate(value))
const promises = Object.entries(situation).map(
async ([dottedName, value]) => {
const paramName = dottedNameParamNameMapping[dottedName]
try {
const serializedValue = serializeEvaluation(
await workerEngine.asyncEvaluateWithEngineId(value)
)
if (typeof serializedValue !== 'undefined') {
searchParams.set(paramName, serializedValue)
} else if (typeof value === 'object') {
searchParams.set(paramName, JSON.stringify(value))
if (typeof serializedValue !== 'undefined') {
searchParams.set(paramName, serializedValue)
} else if (typeof value === 'object') {
searchParams.set(paramName, JSON.stringify(value))
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
// debugger
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
}
})
)
await Promise.all(promises)
searchParams.sort()
return searchParams

View File

@ -1,203 +0,0 @@
import rawRules, { DottedName } from 'modele-social'
import Engine, { Rule } from 'publicodes'
import type { ProviderProps } from '@/components/Provider'
import i18n from './locales/i18n'
import ruleTranslations from './locales/rules-en.yaml'
import translateRules from './locales/translateRules'
type Rules = Record<DottedName, Rule>
const unitsTranslations = Object.entries(
i18n.getResourceBundle('fr', 'units') as Record<string, string>
)
const engineOptions = {
getUnitKey(unit: string): string {
const key = unitsTranslations
.find(([, trans]) => trans === unit)?.[0]
.replace(/_plural$/, '')
return key || unit
},
}
let warnCount = 0
let timeout: NodeJS.Timeout | null = null
const logger = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
warn: (message: string) => {
// console.warn(message)
warnCount++
timeout !== null && clearTimeout(timeout)
timeout = setTimeout(() => {
// eslint-disable-next-line no-console
console.warn('⚠️', warnCount, 'warnings in the engine')
warnCount = 0
}, 1000)
},
error: (message: string) => {
// eslint-disable-next-line no-console
console.error(message)
},
log: (message: string) => {
// eslint-disable-next-line no-console
console.log(message)
},
}
export function engineFactory(rules: Rules, options = {}) {
return new Engine(rules, { ...engineOptions, ...options, logger })
}
//
export type Actions =
| {
action: 'init'
params: [{ basename: ProviderProps['basename'] }]
result: void
}
| {
action: 'setSituation'
params: Parameters<Engine<DottedName>['setSituation']>
result: void
}
| {
action: 'evaluate'
params: Parameters<Engine<DottedName>['evaluate']>
result: ReturnType<Engine<DottedName>['evaluate']>
}
| {
action: 'getRule'
params: Parameters<Engine<DottedName>['getRule']>
result: ReturnType<Engine<DottedName>['getRule']>
}
| {
action: 'getParsedRules'
params: []
result: ReturnType<Engine<DottedName>['getParsedRules']>
}
| {
action: 'shallowCopy'
params: []
result: void
}
| {
action: 'deleteShallowCopy'
params: [{ engineId: number }]
result: void
}
type GenericParams = {
/**
* The id of the engine to use, the default engine is 0
*/
engineId?: number
/**
* The id of the message, used to identify the response
*/
id: number
}
export type Action<T extends Actions['action']> = Extract<
Actions,
{ action: T }
>
let engines: (Engine<DottedName> | undefined)[] = []
let setDefaultEngineReady: (() => void) | null = null
const isDefaultEngineReady = new Promise(
(resolve) => (setDefaultEngineReady = resolve as () => void)
)
onmessage = async (e) => {
console.log('[onmessage]', e.data)
const { engineId = 0, id, action, params } = e.data as Actions & GenericParams
try {
if (action === 'init') {
const [{ basename }] = params
try {
let rules = rawRules
if (basename === 'infrance') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
rules = translateRules('en', ruleTranslations, rules)
}
const engineId = engines.length
engines.push(engineFactory(rules))
console.log('[engine ready]', engines[engineId])
postMessage({ engineId, id })
setDefaultEngineReady?.()
} catch (e) {
console.error('[error]', e)
// postMessage('error')
}
return
}
await isDefaultEngineReady
const engine = engines[engineId]
if (!engine) {
throw new Error('Engine does not exist')
}
if (action === 'setSituation') {
// safeSetSituation(
// engine,
// ({ situation, <faultyDottedName> }) => {
// console.error('setSituation', { situation, faultyDottedName })
// },
// ...params
// )
engine.setSituation(...params)
return postMessage({ engineId, id })
} else if (action === 'evaluate') {
const result = engine.evaluate(...params)
console.log('[result]', result)
return postMessage({ engineId, id, result })
} else if (action === 'getRule') {
const result = engine.getRule(...params)
return postMessage({ engineId, id, result })
} else if (action === 'getParsedRules') {
const result = engine.getParsedRules()
return postMessage({ engineId, id, result })
} else if (action === 'shallowCopy') {
const result = engine.shallowCopy()
engines.push(result)
return postMessage({ engineId: engines.length - 1, id })
} else if (action === 'deleteShallowCopy') {
if (engineId === 0) {
throw new Error('Cannot delete the default engine')
}
delete engines[engineId]
// false positive warning from eslint
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const lastIndex: number = engines.findLastIndex(
(el) => el instanceof Engine
)
engines = lastIndex >= 0 ? engines.splice(0, lastIndex) : engines
console.log('[engines]', engines)
return postMessage({ engineId, id })
} else {
console.log('[Message inconu]', e.data)
}
} catch (error) {
return postMessage({ engineId, id, error })
}
}

View File

@ -7,11 +7,17 @@ import i18next from '../locales/i18n'
import '../api/sentry'
export const AppFr = () => (
<I18nProvider locale="fr-FR">
<App basename="mon-entreprise" />
</I18nProvider>
)
import { useCreateWorkerEngine } from '@/worker/socialWorkerEngineClient'
export const AppFr = () => {
// useCreateWorkerEngine('mon-entreprise')
return (
<I18nProvider locale="fr-FR">
<App basename="mon-entreprise" />
</I18nProvider>
)
}
const AppFrWithProfiler = withProfiler(AppFr)

View File

@ -1,5 +1,5 @@
import { DottedName } from 'modele-social'
import Engine from 'publicodes'
import Engine, { RuleNode } from 'publicodes'
import { useMemo } from 'react'
import { useSelector } from 'react-redux'
@ -10,8 +10,13 @@ import {
useMissingVariables,
} from '@/store/selectors/simulationSelectors'
import { ImmutableType } from '@/types/utils'
import {
usePromiseOnSituationChange,
useWorkerEngine,
WorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import { useEngine } from '../components/utils/EngineContext'
// import { useEngine } from '../components/utils/EngineContext'
type MissingVariables = Partial<Record<DottedName, number>>
@ -67,23 +72,30 @@ export function getNextQuestions(
}
export const useNextQuestions = function (
engines?: Array<Engine<DottedName>>
workerEngines?: WorkerEngine[]
): Array<DottedName> {
const answeredQuestions = useSelector(answeredQuestionsSelector)
const config = useSelector(configSelector)
const engine = useEngine()
const missingVariables = useMissingVariables({ engines: engines ?? [engine] })
const nextQuestions = useMemo(() => {
const next = getNextQuestions(
missingVariables,
config.questions ?? {},
answeredQuestions
)
const workerEngine = useWorkerEngine()
const missingVariables = useMissingVariables(workerEngines)
return next.filter(
(question) => engine.getRule(question).rawNode.question !== undefined
)
}, [missingVariables, config, answeredQuestions, engine])
const nextQuestions = usePromiseOnSituationChange(
async () => {
const next = getNextQuestions(
missingVariables,
config.questions ?? {},
answeredQuestions
)
const rules = await Promise.all(
next.map((question) => workerEngine.asyncGetRuleWithEngineId(question))
)
return next.filter((_, i) => rules[i].rawNode.question !== undefined)
},
[missingVariables, config.questions, answeredQuestions, workerEngine],
{ defaultValue: [] as DottedName[] }
)
return nextQuestions
}

View File

@ -6,7 +6,7 @@ import { DependencyList, useCallback, useEffect, useState } from 'react'
*/
export const usePromise = <T, Default = undefined>(
promise: () => Promise<T>,
deps: DependencyList = [],
deps: DependencyList,
defaultValue?: Default
) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -15,9 +15,35 @@ export const usePromise = <T, Default = undefined>(
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => void lazyPromise(), deps)
// cancelable promise
// useEffect(() => {
// const controller = new window.AbortController()
// const signal = controller.signal
// void new Promise((resolve, reject) => {
// void lazyPromise().then(resolve)
// // .then(resolve)
// signal.addEventListener('abort', () => {
// reject(new Error('Promise aborted'))
// })
// })
// return () => {
// console.log('### aborting')
// controller.abort()
// // promise.
// }
// }, deps)
return state
}
/**
* Return a typed tuple.
*/
const tuple = <T extends unknown[]>(args: [...T]): T => args
/**
* Execute an asynchronous function and return its result (Return default value if the promise is not finished).
* Use this hook if you want to fire the promise manually.
@ -25,19 +51,16 @@ export const usePromise = <T, Default = undefined>(
export const useLazyPromise = <
T,
Params extends unknown[],
Default = undefined
Default = undefined,
>(
promise: (...params: Params) => Promise<T>,
deps: DependencyList = [],
deps: DependencyList,
defaultValue?: Default
) => {
// console.log('===>', defaultValue)
const [state, setState] = useState<T | Default>(defaultValue as Default)
const lazyPromise = useCallback(
async (...params: Params) => {
// console.log('====', defaultValue)
const result = await promise(...params)
setState(result)
@ -47,5 +70,5 @@ export const useLazyPromise = <
deps
)
return [state, lazyPromise] as const
return tuple([state, lazyPromise])
}

View File

@ -76,11 +76,11 @@ export default function Landing() {
justifyContent: 'center',
}}
>
<SimulateurCard {...simulators.salarié} />
{/* <SimulateurCard {...simulators.salarié} /> */}
<SimulateurCard {...simulators['auto-entrepreneur']} />
<SimulateurCard {...simulators['comparaison-statuts']} />
{/* <SimulateurCard {...simulators['comparaison-statuts']} /> */}
<Grid
item

View File

@ -10,7 +10,6 @@ import {
import { CompanyDetails } from '@/components/company/Details'
import { CompanySearchField } from '@/components/company/SearchField'
import { ForceThemeProvider } from '@/components/utils/DarkModeContext'
import { useEngine } from '@/components/utils/EngineContext'
import AnswerGroup from '@/design-system/answer-group'
import { Button } from '@/design-system/buttons'
import { Emoji } from '@/design-system/emoji'
@ -22,6 +21,7 @@ import { useSetEntreprise } from '@/hooks/useSetEntreprise'
import { useSitePaths } from '@/sitePaths'
import { getCookieValue } from '@/storage/readCookie'
import { resetCompany } from '@/store/actions/companyActions'
import { usePromiseOnSituationChange } from '@/worker/socialWorkerEngineClient'
// import { RootState } from '@/store/reducers/rootReducer'
@ -30,7 +30,10 @@ export default function SearchOrCreate() {
// const statutChoisi = useSelector(
// (state: RootState) => state.choixStatutJuridique.companyStatusChoice
// )
const companySIREN = useEngine().evaluate('entreprise . SIREN').nodeValue
const companySIREN = usePromiseOnSituationChange(
() => asyncEvaluate('entreprise . SIREN'),
[]
)?.nodeValue
useSetEntrepriseFromUrssafConnection()
const handleCompanySubmit = useHandleCompanySubmit()
const dispatch = useDispatch()
@ -166,7 +169,11 @@ function useHandleCompanySubmit() {
function useSetEntrepriseFromUrssafConnection() {
const setEntreprise = useSetEntreprise()
const siret = siretFromUrssafFrConnection()
const companySIREN = useEngine().evaluate('entreprise . SIREN').nodeValue
const companySIREN = usePromiseOnSituationChange(
() => asyncEvaluate('entreprise . SIREN'),
[]
)?.nodeValue
useEffect(() => {
if (siret && !companySIREN) {
searchDenominationOrSiren(siret)

View File

@ -8,7 +8,7 @@ import { styled } from 'styled-components'
import { ExplicableRule } from '@/components/conversation/Explicable'
import RuleInput from '@/components/conversation/RuleInput'
import { FadeIn } from '@/components/ui/animate'
import { EngineContext } from '@/components/utils/EngineContext'
// import { EngineContext } from '@/components/utils/EngineContext'
import { Markdown } from '@/components/utils/markdown'
import { Spacing } from '@/design-system/layout'
import { H3 } from '@/design-system/typography/heading'
@ -19,7 +19,7 @@ import {
situationSelector,
targetUnitSelector,
} from '@/store/selectors/simulationSelectors'
import { evaluateQuestion, getMeta } from '@/utils'
import { getMeta } from '@/utils'
type SubSectionProp = {
dottedName: DottedName
@ -96,7 +96,7 @@ export function SimpleField(props: SimpleFieldProps) {
)
let displayedQuestion =
question ?? evaluateQuestion(engine, engine.getRule(dottedName))
'question ?? evaluateQuestion(engine, engine.getRule(dottedName))'
const labelId = useSSRSafeId()
const targetUnit = useSelector(targetUnitSelector)

View File

@ -4,7 +4,7 @@ import { styled } from 'styled-components'
import Value, { Condition } from '@/components/EngineValue'
import { FromTop } from '@/components/ui/animate'
import { useEngine } from '@/components/utils/EngineContext'
// import { useEngine } from '@/components/utils/EngineContext'
import { Markdown } from '@/components/utils/markdown'
import { Article } from '@/design-system/card'
import { Emoji } from '@/design-system/emoji'

View File

@ -1,13 +1,12 @@
import { DottedName } from 'modele-social'
import Engine, { PublicodesExpression } from 'publicodes'
import { Fragment, lazy, Suspense, useCallback, useContext } from 'react'
import { Fragment, lazy, Suspense, useCallback } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { TrackPage } from '@/components/ATInternetTracking'
import RuleInput from '@/components/conversation/RuleInput'
import { WhenApplicable, WhenNotApplicable } from '@/components/EngineValue'
import BrowserOnly from '@/components/utils/BrowserOnly'
import { EngineContext, EngineProvider } from '@/components/utils/EngineContext'
import { Markdown } from '@/components/utils/markdown'
import { usePersistingState } from '@/components/utils/persistState'
import { Button } from '@/design-system/buttons'
@ -17,13 +16,7 @@ import PopoverConfirm from '@/design-system/popover/PopoverConfirm'
import { headings } from '@/design-system/typography'
import { Intro, SmallBody } from '@/design-system/typography/paragraphs'
import useSimulationConfig from '@/hooks/useSimulationConfig'
import {
buildSituationFromObject,
evaluateQuestion,
getMeta,
hash,
omit,
} from '@/utils'
import { buildSituationFromObject, getMeta, hash, omit } from '@/utils'
import formulaire from './demande-mobilité.yaml'
@ -45,9 +38,9 @@ export default function PageMobilité() {
certificat A1 afin d'être couverts pendant la période de travail à
l'étranger.
</Intro>
<EngineProvider value={engine}>
{/* <EngineProvider value={engine}>
<FormulairePublicodes />
</EngineProvider>
</EngineProvider> */}
</>
)
}
@ -74,7 +67,7 @@ const useFields = (
const VERSION = hash(JSON.stringify(formulaire))
function FormulairePublicodes() {
const engine = useContext(EngineContext)
const engine = useEngine()
const [situation, setSituation] = usePersistingState<
Record<string, PublicodesExpression>
>(`formulaire-détachement:${VERSION}`, {})
@ -181,12 +174,12 @@ function FormulairePublicodes() {
}}
>
{' '}
<Markdown>
{/* <Markdown>
{evaluateQuestion(
engine,
engine.getRule(dottedName)
) ?? ''}
</Markdown>
</Markdown> */}
</div>
)}
@ -194,10 +187,10 @@ function FormulairePublicodes() {
id={dottedName.replace(/\s|\./g, '_')}
dottedName={dottedName as DottedName}
onChange={(value) => onChange(dottedName, value)}
aria-label={
question &&
evaluateQuestion(engine, engine.getRule(dottedName))
}
// aria-label={
// question &&
// evaluateQuestion(engine, engine.getRule(dottedName))
// }
/>
{question && type === undefined && description && (
<Markdown>{description}</Markdown>

View File

@ -1,37 +1,43 @@
import { useTranslation } from 'react-i18next'
import { useEngine } from '@/components/utils/EngineContext'
import { Article } from '@/design-system/card'
import {
usePromiseOnSituationChange,
useWorkerEngine,
} from '@/worker/socialWorkerEngineClient'
export function AnnuaireEntreprises() {
const { t } = useTranslation()
const engine = useEngine()
const workerEngine = useWorkerEngine()
const siren = engine.evaluate('entreprise . SIREN').nodeValue as string
const siren = usePromiseOnSituationChange(
async () =>
await workerEngine.asyncEvaluateWithEngineId('entreprise . SIREN'),
[workerEngine],
{ defaultValue: null }
)?.nodeValue as string | null
return (
<>
<Article
title={t(
'assistants.pour-mon-entreprise.annuaire-entreprises.title',
'Voir vos données publiques'
)}
href={`https://annuaire-entreprises.data.gouv.fr/entreprise/${siren}?mtm_campaign=mon-entreprise`}
ctaLabel={t(
'assistants.pour-mon-entreprise.annuaire-entreprises.cta',
'Visiter le site'
)}
aria-label={t(
'assistants.pour-mon-entreprise.annuaire-entreprises.aria-label',
'Annuaire-entreprise, Visiter le site'
)}
>
{t(
'assistants.pour-mon-entreprise.annuaire-entreprises.body',
'Retrouvez toutes les informations publiques concernant votre entreprise sur'
)}{' '}
Annuaire des Entreprises.
</Article>
</>
)
return typeof siren === 'string' ? (
<Article
title={t(
'assistants.pour-mon-entreprise.annuaire-entreprises.title',
'Voir vos données publiques'
)}
href={`https://annuaire-entreprises.data.gouv.fr/entreprise/${siren}?mtm_campaign=mon-entreprise`}
ctaLabel={t(
'assistants.pour-mon-entreprise.annuaire-entreprises.cta',
'Visiter le site'
)}
aria-label={t(
'assistants.pour-mon-entreprise.annuaire-entreprises.aria-label',
'Annuaire-entreprise, Visiter le site'
)}
>
{t(
'assistants.pour-mon-entreprise.annuaire-entreprises.body',
'Retrouvez toutes les informations publiques concernant votre entreprise sur'
)}{' '}
Annuaire des Entreprises.
</Article>
) : null
}

View File

@ -30,7 +30,7 @@ export function CodeDuTravailNumeriqueCard() {
Pour toutes vos questions en droit du travail, rendez-vous sur le site
Code du travail numérique
</Trans>
<Spacing md />
<Spacing md as="span" style={{ display: 'block' }} />
<CodeDuTravailNumeriqueLogo />
</Article>
)

View File

@ -29,7 +29,6 @@ import { SimulateurCard } from '@/components/SimulateurCard'
import { FromTop } from '@/components/ui/animate'
import { ForceThemeProvider } from '@/components/utils/DarkModeContext'
import { useEngine } from '@/components/utils/EngineContext'
import { Markdown } from '@/components/utils/markdown'
import { Message, Popover } from '@/design-system'
import { Button } from '@/design-system/buttons'
import { Container, Grid, Spacing } from '@/design-system/layout'
@ -44,7 +43,6 @@ import { useSitePaths } from '@/sitePaths'
import { resetCompany } from '@/store/actions/companyActions'
import { SimulationConfig } from '@/store/reducers/rootReducer'
import { companySituationSelector } from '@/store/selectors/simulationSelectors'
import { evaluateQuestion } from '@/utils'
import forms from './forms.svg'
import growth from './growth.svg'
@ -294,9 +292,9 @@ const AskCompanyMissingDetails = () => {
{questions.map((question) => (
<FromTop key={question.dottedName}>
<H3>
<Markdown options={{ forceInline: true }}>
{/* <Markdown options={{ forceInline: true }}>
{evaluateQuestion(engine, question) ?? ''}
</Markdown>
</Markdown> */}
</H3>
<RuleInput
dottedName={question.dottedName}

View File

@ -102,7 +102,7 @@ export default function SearchCodeAPE({
[]
)
const lazyData = usePromise(() => import('@/public/data/ape-search.json'))
const lazyData = usePromise(() => import('@/public/data/ape-search.json'), [])
const lastIdxs = useRef<Record<string, UFuzzy.HaystackIdxs>>({})
const prevValue = useRef<string>(searchQuery)

View File

@ -1,35 +1,36 @@
import { ImmutableType } from '@/types/utils'
import type { ImmutableType } from '@/types/utils'
import { choixStatutJuridiqueConfig } from '../assistants/choix-du-statut/config'
import { déclarationChargesSocialesIndépendantConfig } from '../assistants/declaration-charges-sociales-independant/config'
import { demandeMobilitéConfig } from '../assistants/demande-mobilité/config'
import { économieCollaborativeConfig } from '../assistants/économie-collaborative/config'
import { pourMonEntrepriseConfig } from '../assistants/pour-mon-entreprise/config'
import { rechercheCodeApeConfig } from '../assistants/recherche-code-ape/config'
// import { choixStatutJuridiqueConfig } from '../assistants/choix-du-statut/config'
// import { déclarationChargesSocialesIndépendantConfig } from '../assistants/declaration-charges-sociales-independant/config'
// import { demandeMobilitéConfig } from '../assistants/demande-mobilité/config'
// import { économieCollaborativeConfig } from '../assistants/économie-collaborative/config'
// import { pourMonEntrepriseConfig } from '../assistants/pour-mon-entreprise/config'
// import { rechercheCodeApeConfig } from '../assistants/recherche-code-ape/config'
import { PageConfig, SimulatorsDataParams } from '../simulateurs/_configs/types'
import { artisteAuteurConfig } from '../simulateurs/artiste-auteur/config'
// import { artisteAuteurConfig } from '../simulateurs/artiste-auteur/config'
import { autoEntrepreneurConfig } from '../simulateurs/auto-entrepreneur/config'
import { auxiliaireMédicalConfig } from '../simulateurs/auxiliaire-médical/config'
import { avocatConfig } from '../simulateurs/avocat/config'
import { chirurgienDentisteConfig } from '../simulateurs/chirurgien-dentiste/config'
import { chômagePartielConfig } from '../simulateurs/chômage-partiel/config'
import { cipavConfig } from '../simulateurs/cipav/config'
import { comparaisonStatutsConfig } from '../simulateurs/comparaison-statuts/config'
import { coûtCréationEntrepriseConfig } from '../simulateurs/cout-creation-entreprise/config.js'
import { dividendesConfig } from '../simulateurs/dividendes/config'
import { eirlConfig } from '../simulateurs/eirl/config'
import { entrepriseIndividuelleConfig } from '../simulateurs/entreprise-individuelle/config'
import { eurlConfig } from '../simulateurs/eurl/config'
import { expertComptableConfig } from '../simulateurs/expert-comptable/config'
import { impôtSociétéConfig } from '../simulateurs/impot-societe/config'
import { indépendantConfig } from '../simulateurs/indépendant/config'
import { médecinConfig } from '../simulateurs/médecin/config'
import { pamcConfig } from '../simulateurs/pamc/config'
import { pharmacienConfig } from '../simulateurs/pharmacien/config'
import { professionLibéraleConfig } from '../simulateurs/profession-libérale/config'
import { sageFemmeConfig } from '../simulateurs/sage-femme/config'
import { salariéConfig } from '../simulateurs/salarié/config'
import { sasuConfig } from '../simulateurs/sasu/config'
// import { auxiliaireMédicalConfig } from '../simulateurs/auxiliaire-médical/config'
// import { avocatConfig } from '../simulateurs/avocat/config'
// import { chirurgienDentisteConfig } from '../simulateurs/chirurgien-dentiste/config'
// import { chômagePartielConfig } from '../simulateurs/chômage-partiel/config'
// import { cipavConfig } from '../simulateurs/cipav/config'
// import { comparaisonStatutsConfig } from '../simulateurs/comparaison-statuts/config'
// import { coûtCréationEntrepriseConfig } from '../simulateurs/cout-creation-entreprise/config.js'
// import { dividendesConfig } from '../simulateurs/dividendes/config'
// import { eirlConfig } from '../simulateurs/eirl/config'
// import { entrepriseIndividuelleConfig } from '../simulateurs/entreprise-individuelle/config'
// import { eurlConfig } from '../simulateurs/eurl/config'
// import { expertComptableConfig } from '../simulateurs/expert-comptable/config'
// import { impôtSociétéConfig } from '../simulateurs/impot-societe/config'
// import { indépendantConfig } from '../simulateurs/indépendant/config'
// import { médecinConfig } from '../simulateurs/médecin/config'
// import { pamcConfig } from '../simulateurs/pamc/config'
// import { pharmacienConfig } from '../simulateurs/pharmacien/config'
// import { professionLibéraleConfig } from '../simulateurs/profession-libérale/config'
// import { sageFemmeConfig } from '../simulateurs/sage-femme/config'
// import { salariéConfig } from '../simulateurs/salarié/config'
// import { sasuConfig } from '../simulateurs/sasu/config'
/**
* Contient l'intégralité des données concernant les différents simulateurs et assistants
@ -39,37 +40,37 @@ import { sasuConfig } from '../simulateurs/sasu/config'
const getMetadataSrc = (params: SimulatorsDataParams) => {
const data = {
// simulateurs:
...salariéConfig(params),
...entrepriseIndividuelleConfig(params),
...eirlConfig(params),
...sasuConfig(params),
...eurlConfig(params),
// ...salariéConfig(params),
// ...entrepriseIndividuelleConfig(params),
// ...eirlConfig(params),
// ...sasuConfig(params),
// ...eurlConfig(params),
...autoEntrepreneurConfig(params),
...indépendantConfig(params),
...artisteAuteurConfig(params),
...chômagePartielConfig(params),
...comparaisonStatutsConfig(params),
...économieCollaborativeConfig(params),
...pharmacienConfig(params),
...médecinConfig(params),
...chirurgienDentisteConfig(params),
...sageFemmeConfig(params),
...auxiliaireMédicalConfig(params),
...avocatConfig(params),
...expertComptableConfig(params),
...professionLibéraleConfig(params),
...pamcConfig(params),
...dividendesConfig(params),
...coûtCréationEntrepriseConfig(params),
...impôtSociétéConfig(params),
...cipavConfig(params),
// ...indépendantConfig(params),
// ...artisteAuteurConfig(params),
// ...chômagePartielConfig(params),
// ...comparaisonStatutsConfig(params),
// ...économieCollaborativeConfig(params),
// ...pharmacienConfig(params),
// ...médecinConfig(params),
// ...chirurgienDentisteConfig(params),
// ...sageFemmeConfig(params),
// ...auxiliaireMédicalConfig(params),
// ...avocatConfig(params),
// ...expertComptableConfig(params),
// ...professionLibéraleConfig(params),
// ...pamcConfig(params),
// ...dividendesConfig(params),
// ...coûtCréationEntrepriseConfig(params),
// ...impôtSociétéConfig(params),
// ...cipavConfig(params),
// assistants:
...choixStatutJuridiqueConfig(params),
...déclarationChargesSocialesIndépendantConfig(params),
...demandeMobilitéConfig(params),
...pourMonEntrepriseConfig(params),
...rechercheCodeApeConfig(params),
// // assistants:
// ...choixStatutJuridiqueConfig(params),
// ...déclarationChargesSocialesIndépendantConfig(params),
// ...demandeMobilitéConfig(params),
// ...pourMonEntrepriseConfig(params),
// ...rechercheCodeApeConfig(params),
} as const
return data satisfies ImmutableType<Record<string, PageConfig>>

View File

@ -1,9 +1,6 @@
import { Trans, useTranslation } from 'react-i18next'
import { Condition, WhenAlreadyDefined } from '@/components/EngineValue'
import { useEngine } from '@/components/utils/EngineContext'
// import { Article } from '@/design-system/card'
// import { Emoji } from '@/design-system/emoji'
import { Grid, Spacing } from '@/design-system/layout'
import { H2 } from '@/design-system/typography/heading'
import {
@ -12,12 +9,16 @@ import {
} from '@/hooks/useCurrentSimulatorData'
import { GuideURSSAFCard } from '@/pages/simulateurs/cards/GuideURSSAFCard'
import { IframeIntegrationCard } from '@/pages/simulateurs/cards/IframeIntegrationCard'
import { SimulatorRessourceCard } from '@/pages/simulateurs/cards/SimulatorRessourceCard'
import { useSitePaths } from '@/sitePaths'
import {
usePromiseOnSituationChange,
useWorkerEngine,
} from '@/worker/socialWorkerEngineClient'
import { AnnuaireEntreprises } from '../assistants/pour-mon-entreprise/AnnuaireEntreprises'
import { AutoEntrepreneurCard } from '../assistants/pour-mon-entreprise/AutoEntrepeneurCard'
import { CodeDuTravailNumeriqueCard } from '../assistants/pour-mon-entreprise/CodeDuTravailNumeriqueCard'
import { SimulatorRessourceCard } from './cards/SimulatorRessourceCard'
interface NextStepsProps {
iframePath?: MergedSimulatorDataValues['iframePath']
@ -27,11 +28,19 @@ interface NextStepsProps {
export function NextSteps({ iframePath, nextSteps }: NextStepsProps) {
const { absoluteSitePaths } = useSitePaths()
const { language } = useTranslation().i18n
const engine = useEngine()
const workerEngine = useWorkerEngine()
const { key } = useCurrentSimulatorData()
const guideUrssaf = guidesUrssaf.find(
({ associatedRule }) => engine.evaluate(associatedRule).nodeValue
const guideUrssaf = usePromiseOnSituationChange(
async () =>
(
await Promise.all(
guidesUrssaf.map(({ associatedRule }) =>
workerEngine.asyncEvaluateWithEngineId(associatedRule)
)
)
).find(({ nodeValue }) => nodeValue),
[workerEngine]
)
if (!iframePath && !guideUrssaf) {
@ -58,7 +67,7 @@ export function NextSteps({ iframePath, nextSteps }: NextStepsProps) {
{nextSteps &&
nextSteps.map((simulatorId) => (
<Grid item xs={12} sm={6} lg={4} key={simulatorId} role="listitem">
<SimulatorRessourceCard simulatorId={simulatorId} />
{/* <SimulatorRessourceCard simulatorId={simulatorId} /> */}
</Grid>
))}
@ -70,14 +79,14 @@ export function NextSteps({ iframePath, nextSteps }: NextStepsProps) {
/>
</Grid>
)}
{key === 'salarié' && (
{/* {key === 'salarié' && (
<Grid item xs={12} sm={6} lg={4} role="listitem">
<CodeDuTravailNumeriqueCard />
</Grid>
)}
)} */}
{guideUrssaf && language === 'fr' && (
<Grid item xs={12} sm={6} lg={4} role="listitem">
<GuideURSSAFCard guideUrssaf={guideUrssaf} />
{/* <GuideURSSAFCard guideUrssaf={guideUrssaf} /> */}
</Grid>
)}
</Grid>

View File

@ -1,8 +1,8 @@
import type { TFunction } from 'i18next'
import { DottedName } from 'modele-social'
import { ASTNode, PublicodesExpression } from 'publicodes'
import type { DottedName } from 'modele-social'
import type { ASTNode, PublicodesExpression } from 'publicodes'
import { AbsoluteSitePaths } from '@/sitePaths'
import type { AbsoluteSitePaths } from '@/sitePaths'
export type Situation = Partial<
Record<DottedName, PublicodesExpression | ASTNode>

View File

@ -19,8 +19,11 @@ import { H2 } from '@/design-system/typography/heading'
import { Body } from '@/design-system/typography/paragraphs'
export default function AutoEntrepreneur() {
console.log('ok')
return (
<>
aaaaaaaaaaaaaaa
<Simulation
explanations={<Explanation />}
afterQuestionsSlot={<SelectSimulationYear />}

View File

@ -1,10 +1,15 @@
import { DottedName } from 'modele-social'
import Engine, { utils } from 'publicodes'
import { utils } from 'publicodes'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
import { useEngine } from '@/components/utils/EngineContext'
import { usePromise } from '@/hooks/usePromise'
// import { useEngine } from '@/components/utils/EngineContext'
import { RootState, Situation } from '@/store/reducers/rootReducer'
import {
useWorkerEngine,
WorkerEngine,
} from '@/worker/socialWorkerEngineClient'
export const configSelector = (state: RootState) =>
state.simulation?.config ?? {}
@ -22,22 +27,34 @@ export const configObjectifsSelector = createSelector(
const emptySituation: Situation = {}
export const useMissingVariables = ({
engines,
}: {
engines: Array<Engine<DottedName>>
}): Partial<Record<DottedName, number>> => {
export const useMissingVariables = (
workerEngines?: WorkerEngine[]
): Partial<Record<DottedName, number>> => {
const objectifs = useSelector(configObjectifsSelector)
const workerEngine = useWorkerEngine()
return treatAPIMissingVariables(
objectifs
.flatMap((objectif) =>
engines.map((e) => e.evaluate(objectif).missingVariables ?? {})
return usePromise(
async () => {
const evaluates = await Promise.all(
objectifs.flatMap((objectif) =>
(workerEngines ?? [workerEngine]).map(
async (e) =>
(await e.asyncEvaluateWithEngineId(objectif)).missingVariables ??
{}
)
)
)
.reduce(mergeMissing, {}),
useEngine()
return await treatAPIMissingVariables(
evaluates.reduce(mergeMissing, {}),
workerEngine
)
},
[objectifs, workerEngine, workerEngines],
{}
)
}
export const situationSelector = (state: RootState) =>
state.simulation?.situation ?? emptySituation
@ -75,14 +92,26 @@ export const shouldFocusFieldSelector = (state: RootState) =>
*
* For instance, the commune field (API) will fill `commune . nom` `commune . taux versement transport`, `commune . département`, etc.
*/
function treatAPIMissingVariables<Name extends string>(
missingVariables: Partial<Record<Name, number>>,
engine: Engine<Name>
): Partial<Record<Name, number>> {
return (Object.entries(missingVariables) as Array<[Name, number]>).reduce(
(missings, [name, value]: [Name, number]) => {
const parentName = utils.ruleParent(name) as Name
if (parentName && engine.getRule(parentName).rawNode.API) {
async function treatAPIMissingVariables(
missingVariables: Partial<Record<DottedName, number>>,
workerEngine: WorkerEngine
): Promise<Partial<Record<DottedName, number>>> {
return (
await Promise.all(
(Object.entries(missingVariables) as [DottedName, number][]).map(
async ([name, value]) => {
const parentName = utils.ruleParent(name) as DottedName
const rule =
parentName &&
(await workerEngine.asyncGetRuleWithEngineId(parentName))
return [name, value, parentName, rule.rawNode.API] as const
}
)
)
).reduce(
(missings, [name, value, parentName, API]) => {
if (API) {
missings[parentName] = (missings[parentName] ?? 0) + value
return missings
@ -91,9 +120,10 @@ function treatAPIMissingVariables<Name extends string>(
return missings
},
{} as Partial<Record<Name, number>>
{} as Partial<Record<DottedName, number>>
)
}
const mergeMissing = (
left: Record<string, number> | undefined = {},
right: Record<string, number> | undefined = {}

View File

@ -13,6 +13,13 @@ type ImmutableIndex<T> = Readonly<{
[K in keyof T]: ImmutableType<T[K]>
}>
/**
* Mutable type
*/
export type Mutable<T> = {
-readonly [K in keyof T]: Mutable<T[K]>
}
/**
* Merge union of object
*
@ -44,3 +51,11 @@ export type ToOptional<T> = Partial<Pick<T, UndefinedProperties<T>>> &
type UndefinedProperties<T> = {
[P in keyof T]-?: undefined extends T[P] ? P : never
}[keyof T]
/**
* Replace the return type of a function
*/
export type ReplaceReturnType<
T extends (...a: never) => unknown,
TNewReturn,
> = (...a: Parameters<T>) => TNewReturn

View File

@ -1,5 +1,5 @@
import { DottedName } from 'modele-social'
import Engine, {
import {
formatValue,
isPublicodesError,
PublicodesExpression,
@ -234,21 +234,6 @@ export async function getIframeOffset(): Promise<number> {
})
}
export function evaluateQuestion(
engine: Engine,
rule: RuleNode
): string | undefined {
const question = rule.rawNode.question as Exclude<
number,
PublicodesExpression
>
if (question && typeof question === 'object') {
return engine.evaluate(question as PublicodesExpression).nodeValue as string
}
return question
}
export function buildSituationFromObject<Names extends string = DottedName>(
contextDottedName: Names,
situationObject: Record<string, PublicodesExpression>

View File

@ -0,0 +1,61 @@
import rawRules, { DottedName } from 'modele-social'
import Engine from 'publicodes'
import type { ProviderProps } from '@/components/Provider'
import i18n from '@/locales/i18n'
import ruleTranslations from '@/locales/rules-en.yaml'
import translateRules from '@/locales/translateRules'
import { createWorkerEngine, WorkerEngineActions } from './workerEngine'
function getUnitKey(unit: string): string {
const units = i18n.getResourceBundle('fr', 'units') as Record<string, string>
const key = Object.entries(units)
.find(([, trans]) => trans === unit)?.[0]
.replace(/_plural$/, '')
return key || unit
}
let warnCount = 0
let timeout: NodeJS.Timeout | null = null
const logger = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
warn: (message: string) => {
// console.warn(message)
warnCount++
timeout !== null && clearTimeout(timeout)
timeout = setTimeout(() => {
// eslint-disable-next-line no-console
console.warn('⚠️', warnCount, 'warnings in the engine')
warnCount = 0
}, 1000)
},
error: (message: string) => {
// eslint-disable-next-line no-console
console.error(message)
},
log: (message: string) => {
// eslint-disable-next-line no-console
console.log(message)
},
}
const init = ({ basename }: Pick<ProviderProps, 'basename'>) => {
let rules = rawRules
if (basename === 'infrance') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
rules = translateRules('en', ruleTranslations, rules)
}
const engine = new Engine(rules, { getUnitKey, logger })
return engine
}
export type Actions = WorkerEngineActions<Parameters<typeof init>, DottedName>
console.time('[createWorkerEngine]')
createWorkerEngine(init)
console.timeEnd('[createWorkerEngine]')

View File

@ -0,0 +1,329 @@
import { DottedName } from 'modele-social'
import {
createContext,
DependencyList,
useContext,
useEffect,
useMemo,
useState,
useTransition,
} from 'react'
import { ProviderProps } from '@/components/Provider'
import { useSetupSafeSituation } from '@/components/utils/EngineContext'
import { useLazyPromise, usePromise } from '@/hooks/usePromise'
import { Actions } from './socialWorkerEngine.worker'
import SocialeWorkerEngine from './socialWorkerEngine.worker?worker'
import {
createWorkerEngineClient,
WorkerEngineClient,
} from './workerEngineClient'
export type WorkerEngine = NonNullable<ReturnType<typeof useCreateWorkerEngine>>
// @ts-expect-error
const WorkerEngineContext = createContext<WorkerEngine>()
// export const useWorkerEngineContext = () => {
// const context = useContext(WorkerEngineContext)
// if (!context) {
// throw new Error(
// 'You are trying to use the worker engine outside of its provider'
// )
// }
// return context
// }
export const useWorkerEngine = () => {
const context = useContext(WorkerEngineContext)
if (!context) {
throw new Error(
'You are trying to use the worker engine outside of its provider'
)
}
// if (!context) {
// throw new Error(
// 'You are trying to use the worker engine before it is ready'
// )
// }
return context
}
export const WorkerEngineProvider = ({
children,
basename,
}: {
children: React.ReactNode
basename: ProviderProps['basename']
}) => {
const workerEngine = useCreateWorkerEngine(basename)
useSetupSafeSituation(workerEngine)
if (workerEngine === undefined) {
return null
}
return (
<WorkerEngineContext.Provider value={workerEngine}>
{children}
</WorkerEngineContext.Provider>
)
}
// export type WorkerEngine = WorkerEngineClient<Actions>
// let workerClient: | null = null
// setTimeout(() => {
// const preparedWorker = new SocialeWorkerEngine()
// const workerClient: WorkerEngineClient<Actions> =
// createWorkerEngineClient<Actions>(
// new SocialeWorkerEngine(),
// () => {},
// // (engineId) =>
// // setSituationVersion((situationVersion) => {
// // // console.log('??? setSituationVersion original')
// // // situationVersion[engineId] =
// // // typeof situationVersion[engineId] !== 'number'
// // // ? 0
// // // : situationVersion[engineId]++
// // // return situationVersion
// // return situationVersion + 1
// // }),
// { basename: 'mon-entreprise' }
// )
// workerClient.test.onSituationChange = function (engineId) {
// console.log('original onSituationChange')
// }
// // }, 50)
// console.time('loading')
/**
* This hook is used to create a worker engine.
* @param basename
*/
export const useCreateWorkerEngine = (basename: ProviderProps['basename']) => {
const [situationVersion, setSituationVersion] = useState(0)
const [workerEngine, setWorkerEngine] =
useState<WorkerEngineClient<Actions>>()
// console.log('llllllpppppppppppppppppppppppppp', workerClient)
const [transition, startTransition] = useTransition()
useEffect(() => {
// workerClient.test.onSituationChange = function (engineId) {
// console.log('transition...')
// startTransition(() => {
// setSituationVersion((situationVersion) => {
// // console.log('??? setSituationVersion original')
// // situationVersion[engineId] =
// // typeof situationVersion[engineId] !== 'number'
// // ? 0
// // : situationVersion[engineId]++
// // return situationVersion
// return situationVersion + 1
// })
// })
// }
const workerClient = createWorkerEngineClient<Actions>(
new SocialeWorkerEngine(),
// () => {},
(engineId) =>
startTransition(() => {
setSituationVersion((situationVersion) => {
// console.log('??? setSituationVersion original')
// situationVersion[engineId] =
// typeof situationVersion[engineId] !== 'number'
// ? 0
// : situationVersion[engineId]++
// return situationVersion
return situationVersion + 1
})
}),
{ basename: 'mon-entreprise' }
)
console.log('{init worker}', workerClient)
setWorkerEngine(workerClient)
void workerClient.asyncSetSituationWithEngineId({})
console.time('{init}')
let init = false
void workerClient.isWorkerReady.finally(() => {
init = true
console.timeEnd('{init}')
})
// example of usage
// void Promise.all([
// workerClient
// .asyncEvaluate('SMIC')
// .then((result) => console.log('{result}', result)),
// workerClient
// .asyncEvaluate('date')
// .then((result) => console.log('{result}', result)),
// ])
return () => {
!init && console.timeEnd('{init}')
console.log('{terminate worker}', workerClient)
// workerClient.terminate()
}
}, [basename])
// return workerEngine ? { ...workerEngine, situationVersion } : null
const memo = useMemo(
() => (workerEngine ? { ...workerEngine, situationVersion } : undefined),
[situationVersion, workerEngine]
)
return memo
}
/**
*
*/
// const useSituationVersion = (workerEngineCtx: WorkerEngineCtx) =>
// workerEngineCtx.situationVersion
// [engineId]
interface Options<Default> {
defaultValue?: Default
workerEngine?: WorkerEngine
}
/**
* Wrapper around usePromise that adds the situation version to the dependencies
* @example const date = usePromiseOnSituationChange(() => asyncEvaluate('date'), []) // date will be updated when the situation changes
* @deprecated
*/
export const usePromiseOnSituationChange = <T, Default = undefined>(
promise: () => Promise<T>,
deps: DependencyList,
{ defaultValue, workerEngine: workerEngineOption }: Options<Default> = {}
): T | Default => {
const defaultWorkerEngineCtx = useWorkerEngine()
const { situationVersion } = workerEngineOption ?? defaultWorkerEngineCtx
// eslint-disable-next-line react-hooks/exhaustive-deps
const state = usePromise(promise, [...deps, situationVersion], defaultValue)
return state
}
/**
* @deprecated
*/
export const useLazyPromiseOnSituationChange = <T, Default = undefined>(
promise: () => Promise<T>,
deps: DependencyList,
{ defaultValue, workerEngine: workerEngineOption }: Options<Default> = {}
): [T | Default, () => Promise<T>] => {
const defaultWorkerEngineCtx = useWorkerEngine()
const { situationVersion } = workerEngineOption ?? defaultWorkerEngineCtx
// eslint-disable-next-line react-hooks/exhaustive-deps
const tuple = useLazyPromise(
promise,
[...deps, situationVersion],
defaultValue
)
return tuple
}
/**
* This hook is used to get a rule in the worker.
* @param dottedName
* @param options
*/
export const useAsyncGetRule = <
// T extends unknown = undefined,
Default = undefined,
>(
dottedName: DottedName,
{ defaultValue, workerEngine: workerEngineOption }: Options<Default> = {}
) => {
const defaultWorkerEngine = useWorkerEngine()
const workerEngine = workerEngineOption ?? defaultWorkerEngine
return usePromiseOnSituationChange(
async () => workerEngine.asyncGetRuleWithEngineId(dottedName),
[dottedName, workerEngine],
{ defaultValue, workerEngine }
)
}
/**
* This hook is used to get parsed rules in the worker.
* @param engineId
*/
export const useAsyncParsedRules = <
Default = undefined, //
>({
defaultValue,
workerEngine: workerEngineOption,
}: Options<Default> = {}) => {
const defaultWorkerEngine = useWorkerEngine()
const workerEngine = workerEngineOption ?? defaultWorkerEngine
return usePromiseOnSituationChange(
async () => workerEngine.asyncGetParsedRulesWithEngineId(),
[workerEngine],
{ defaultValue, workerEngine }
)
}
export const useShallowCopy = (
workerEngine: WorkerEngine
): WorkerEngine | undefined => {
const [situationVersion, setSituationVersion] = useState(0)
// const defaultWorkerEngine = useWorkerEngine()
// const workerEngine = workerEngineParam
// ?? defaultWorkerEngine
// console.log('??? situ version', situationVersion)
const workerEngineCopy = usePromiseOnSituationChange(
async () => {
const copy = await workerEngine.asyncShallowCopyWithEngineId(() => {
// console.log('??? onSituationChange', copy)
setSituationVersion((x) => x + 1)
})
// copy.onSituationChange = (x) => {
// console.log('??? onSituationChange', copy)
// setSituationVersion(x)
// }
// console.log('??? xxxxxxxxxxxxxxxxxxxxxxxxxxx', copy)
return copy
},
[workerEngine],
{ defaultValue: undefined, workerEngine }
)
const memo = useMemo(
() =>
workerEngineCopy ? { ...workerEngineCopy, situationVersion } : undefined,
[situationVersion, workerEngineCopy]
)
return memo
}

View File

@ -0,0 +1,236 @@
import Engine from 'publicodes'
/**
* This file run any publicodes engine in a web worker.
*/
export type WorkerEngineActions<
InitParams extends unknown[],
Name extends string,
> =
| {
action: 'init'
params: InitParams
result: number
}
| {
action: 'setSituation'
params: Parameters<Engine<Name>['setSituation']>
result: void
}
| {
action: 'evaluate'
params: Parameters<Engine<Name>['evaluate']>
result: ReturnType<Engine<Name>['evaluate']>
}
| {
action: 'getRule'
params: Parameters<Engine<Name>['getRule']>
result: ReturnType<Engine<Name>['getRule']>
}
| {
action: 'getParsedRules'
params: []
result: ReturnType<Engine<Name>['getParsedRules']>
}
| {
action: 'shallowCopy'
params: [] // no params cause we use engineId
result: number
}
| {
action: 'deleteShallowCopy'
params: [] // no params cause we use engineId
result: void
}
type DistributiveOmit<T, K extends keyof T> = T extends unknown
? Omit<T, K>
: never
type GenericParams = {
/**
* The id of the engine to use, the default engine is 0
*/
engineId?: number
/**
* The id of the message, used to identify the response
*/
id: number
}
export type WorkerEngineAction<
Acts extends WorkerEngineActions<unknown[], string>,
T extends Acts['action'],
> = Extract<Acts, { action: T }>
export const createWorkerEngine = <
Name extends string,
EngineType extends Engine<Name>,
InitParams extends unknown[] = unknown[],
>(
init: (...params: InitParams) => EngineType
) => {
type Params = DistributiveOmit<
WorkerEngineActions<InitParams, Name> & GenericParams,
'result'
>
let engines: (EngineType | undefined)[] = []
let queue: (Params & { engineId: number })[] = []
let setDefaultEngineReady: (() => void) | null = null
const isDefaultEngineReady = new Promise(
(resolve) => (setDefaultEngineReady = resolve as () => void)
)
const actions = (
data: Params
// & { engines: EngineType[] }
) => {
const { engineId = 0, id, action, params } = data
const engine = engines[engineId]
if (!engine) {
throw new Error('Engine does not exist')
}
if (action === 'setSituation') {
// safeSetSituation(
// engine,
// ({ situation, <faultyDottedName> }) => {
// console.error('setSituation', { situation, faultyDottedName })
// },
// ...params
// )
engine.setSituation(...params)
return { id }
} else if (action === 'evaluate') {
const result = engine.evaluate(...params)
console.log('[result]', result)
return { id, result }
} else if (action === 'getRule') {
const result = engine.getRule(...params)
return { id, result }
} else if (action === 'getParsedRules') {
const result = engine.getParsedRules()
return { id, result }
} else if (action === 'shallowCopy') {
engines.push(engine.shallowCopy() as EngineType)
return { id, result: engines.length - 1 }
} else if (action === 'deleteShallowCopy') {
if (engineId === 0) {
throw new Error('Cannot delete the default engine')
}
delete engines[engineId]
engines = engines.splice(engineId, 1)
console.log('[engines]', engines)
return { id }
} else {
console.log('[unknow message]', data)
return { id }
}
}
let timeout: NodeJS.Timeout | null = null
onmessage = async (e) => {
console.log('[onmessage]', e.data)
const { engineId = 0, id, action, params } = e.data as Params
try {
if (action === 'init') {
// console.log('[init engine]')
// const [{ basename }] = params
try {
// let rules = rawRules
// if (basename === 'infrance') {
// // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
// rules = translateRules('en', ruleTranslations, rules)
// }
// engineFactory(rules)
const engine = init(...params)
const engineId = engines.length
engines.push(engine)
postMessage({ id, result: engineId })
setDefaultEngineReady?.()
console.log('[engine ready]', engines[engineId])
} catch (e) {
console.error('[error]', e)
// postMessage('error')
}
return
}
await isDefaultEngineReady
queue.push({ engineId, id, action, params } as Params & {
engineId: number
})
if (timeout !== null) {
return
}
// timeout !== null && clearTimeout(timeout)
timeout = setTimeout(() => {
const aborts: number[] = []
const setSituationEncountered: boolean[] = []
const filteredQueue = [...queue]
.reverse()
.filter(({ action, engineId, id }) => {
if (action === 'setSituation')
setSituationEncountered[engineId] = true
const keep =
!setSituationEncountered[engineId] ||
(setSituationEncountered[engineId] && action !== 'evaluate')
if (!keep) aborts.push(id)
return keep
})
.reverse()
console.log('[start queue]', queue, filteredQueue)
console.time('bench')
postMessage({
batch: filteredQueue.map((params) => {
try {
const res = actions(params)
return res
} catch (error) {
return { id: params.id, error }
}
}),
})
console.timeEnd('bench')
const error = new Error(
'aborts the action because the situation has changed'
)
postMessage({ batch: aborts.map((id) => ({ id, error })) })
queue = []
timeout = null
}, 50)
} catch (error) {
return postMessage({ id, error })
}
}
}

View File

@ -0,0 +1,347 @@
import type { WorkerEngineAction, WorkerEngineActions } from './workerEngine'
if (import.meta.env.SSR || !window.Worker) {
throw new Error('Worker is not supported in this browser')
}
// const sleepMs = (ms: number) =>
// new Promise((resolve) => setTimeout(resolve, ms))
/**
* This file is a client to communicate with workerEngine.
*/
export type WorkerEngineClient<
Actions extends WorkerEngineActions<InitParams, Name>,
InitParams extends unknown[] = unknown[],
Name extends string = string,
> = ReturnType<typeof createWorkerEngineClient<Actions, InitParams, Name>>
export const createWorkerEngineClient = <
Actions extends WorkerEngineActions<InitParams, Name>,
InitParams extends unknown[] = unknown[],
Name extends string = string,
>(
worker: Worker,
onSituationChange: (engineId: number) => void = () => {},
...initParams: WorkerEngineAction<Actions, 'init'>['params']
) => {
type Action<T extends Actions['action']> = WorkerEngineAction<Actions, T>
const test = {
onSituationChange: (engineId: number) => {},
}
console.log('{createWorker}')
type WorkerEnginePromise<T extends Actions['action'] = Actions['action']> = {
engineId: number
action: T
resolve: (value: unknown) => void
reject: (value: unknown) => void
}
let promises: WorkerEnginePromise[] = []
let lastCleanup: null | NodeJS.Timeout = null
const postMessage = async <T extends Actions['action'], U extends Action<T>>(
engineId: number,
action: T,
...params: U['params']
// ...params: U['params'] extends [] ? [] : U['params']
) => {
console.log('{postMessage}', action, params)
const promiseTimeout = 100000
const warning = setTimeout(() => {
console.log('{promise waiting for too long, aborting!}', action, params)
promises[id].reject?.(new Error('timeout'))
}, promiseTimeout)
lastCleanup !== null && clearInterval(lastCleanup)
lastCleanup = setTimeout(() => {
if (promises.length) {
console.log('{cleanup}', promises.length)
promises = []
lastCleanup = null
}
}, 200000)
const id = promises.length
console.time(`execute-${id}`)
const stack = new Error().stack
const promise = new Promise<U['result']>((resolve, reject) => {
promises[id] = {
engineId,
action,
resolve: (...params: unknown[]) => {
clearTimeout(warning)
return resolve(...(params as Parameters<typeof resolve>))
},
reject: (err) => {
clearTimeout(warning)
console.error(err)
console.error(stack)
console.error(new Error((err as Error).message, { cause: stack }))
return reject(err)
},
}
})
worker.postMessage({ engineId, action, params, id })
return promise
}
worker.onmessageerror = function (e) {
console.log('{onmessageerror}', e)
}
worker.onerror = function (e) {
console.log('{onerror}', e)
}
const ppp = (data) => {
console.timeEnd(`execute-${data.id}`)
if (data.id === 0) {
console.timeEnd('loading')
}
if ('error' in data) {
return promises[data.id].reject?.(data.error)
}
promises[data.id].resolve?.(data.result)
}
worker.onmessage = function (e) {
console.log('{msg}', e.data)
if ('batch' in e.data) {
e.data.batch.forEach((data) => {
ppp(data)
})
} else {
ppp(e.data)
}
}
const engineId = 0
const isWorkerReady = postMessage(engineId, 'init', ...initParams)
const workerEngineConstruct = (
engineId: number,
onSituationChange: (engineId: number) => void = () => {}
) => ({
test,
engineId,
worker,
isWorkerReady,
onSituationChange,
// promises,
postMessage,
terminate: () => {
workerEngine.worker.terminate()
promises.forEach((promise) => promise.reject?.('worker terminated'))
promises = []
},
// withEngineId: (engineId: number, promise: Promise) => {
// const tmp = workerEngine.engineId
// workerEngine.engineId = engineId
// promise()
// workerEngine.engineId = tmp
// },
// asynchronous setSituation function
/**
* This function is used to set the situation in the worker with a specific engineId.
*/
asyncSetSituationWithEngineId: async (
// engineId: number,
...params: Action<'setSituation'>['params']
): Promise<Action<'setSituation'>['result']> => {
// abort every action "evaluate"
console.log(')=>', promises)
// promises.forEach((promise) => {
// if (engineId === promise.engineId && promise.action === 'evaluate') {
// promise.reject?.('abort')
// }
// })
// if (!workerEngine.worker) {
// await sleepMs(10)
// return workerEngine.asyncSetSituationWithEngineId(engineId, ...params)
// }
// console.log('??? engideid', engineId, workerEngine, onSituationChange)
const ret = await workerEngine.postMessage(
engineId,
'setSituation',
...params
)
console.log('testtesttesttesttest', test)
test.onSituationChange(engineId)
return ret
},
/**
* This function is used to set the situation in the worker.
*/
// asyncSetSituation: async (...params: Action<'setSituation'>['params']) =>
// workerEngine.asyncSetSituationWithEngineId(defaultEngineId, ...params),
// asynchronous evaluate function
/**
* This function is used to evaluate a publicodes expression in the worker with a specific engineId.
*/
asyncEvaluateWithEngineId: async (
// engineId: number,
...params: Action<'evaluate'>['params']
): Promise<Action<'evaluate'>['result']> => {
// if (!workerEngine.worker) {
// await sleepMs(10)
// return workerEngine.asyncEvaluateWithEngineId(engineId, ...params)
// }
const promise = await workerEngine.postMessage(
engineId,
'evaluate',
...params
)
// console.trace('{asyncEvaluateWithEngineId}')
return promise
},
/**
* This function is used to evaluate a publicodes expression in the worker.
*/
// asyncEvaluate: async (...params: Action<'evaluate'>['params']) =>
// workerEngine.asyncEvaluateWithEngineId(defaultEngineId, ...params),
// asynchronous getRule function:
/**
* This function is used to get a publicodes rule that is in the worker with a specific EngineId.
*/
asyncGetRuleWithEngineId: async (
// engineId: number,
...params: Action<'getRule'>['params']
): Promise<Action<'getRule'>['result']> => {
// if (!workerEngine.worker) {
// await sleepMs(10)
// return workerEngine.asyncGetRuleWithEngineId(engineId, ...params)
// }
return await workerEngine.postMessage(engineId, 'getRule', ...params)
},
/**
* This function is used to get a rule in the worker.
*/
// asyncGetRule: async (...params: Action<'getRule'>['params']) =>
// workerEngine.asyncGetRuleWithEngineId(defaultEngineId, ...params),
// asynchronous getParsedRules function
/**
* This function is used to get all the parsed rules in the worker with a specific engineId.
*/
asyncGetParsedRulesWithEngineId: async () // engineId: number
: Promise<Action<'getParsedRules'>['result']> => {
// if (!workerEngine.worker) {
// await sleepMs(10)
// return workerEngine.asyncGetParsedRulesWithEngineId(engineId)
// }
return await workerEngine.postMessage(engineId, 'getParsedRules')
},
/**
* This function is used to get all the parsed rules in the worker.
*/
// asyncGetParsedRules: async () =>
// workerEngine.asyncGetParsedRulesWithEngineId(defaultEngineId),
// asynchronous shallowCopy function
/**
* This function is used to shallow copy an engine in the worker with a specific engineId.
*/
asyncShallowCopyWithEngineId: async (
onSituationChange: () => void = () => {}
) => {
// engineId: number
// if (!workerEngine.worker) {
// await sleepMs(10)
// return workerEngine.asyncShallowCopyWithEngineId(engineId)
// }
const newEngineId = await workerEngine.postMessage(
engineId,
'shallowCopy'
)
// return {
// ...workerEngine,
// engineId: newEngineId,
// }
// const uu = Object.assign({}, workerEngine)
// uu.engineId = newEngineId
// console.log('???[newEngineId]', newEngineId, engineId)
return workerEngineConstruct(newEngineId, onSituationChange)
},
/**
* This function is used to shallow copy an engine in the worker.
*/
// asyncShallowCopy: async () => ({
// ...context,
// engineId: await workerEngine.asyncShallowCopyWithEngineId(
// defaultEngineId
// ),
// }),
// asynchronous deleteShallowCopy function
/**
* * This function is used to delete a shallow copy of an engine in the worker.
*/
asyncDeleteShallowCopy: async () // engineId: number
: Promise<Action<'deleteShallowCopy'>['result']> => {
// if (!workerEngine.worker) {
// await sleepMs(10)
// return workerEngine.asyncDeleteShallowCopy(engineId)
// }
return await workerEngine.postMessage(engineId, 'deleteShallowCopy')
},
})
const workerEngine = workerEngineConstruct(engineId, onSituationChange)
return workerEngine
}

View File

@ -1,350 +0,0 @@
import { DottedName } from 'modele-social'
import React, { DependencyList, useContext, useEffect, useState } from 'react'
import { ProviderProps } from './components/Provider'
import type { Action, Actions } from './engine.worker'
import EngineWorker from './engine.worker?worker'
import { usePromise } from './hooks/usePromise'
if (!window.Worker) {
throw new Error('Worker is not supported in this browser')
}
interface WorkerEngine {
worker: Worker
postMessage: <T extends Actions['action'], U extends Action<T>>(
engineId: number,
action: T,
...params: U['params']
) => Promise<U['result']>
isWorkerReady: Promise<void>
promises: {
resolve: (value: unknown) => void
reject: (value: unknown) => void
}[]
}
const initWorkerEngine = (
basename: ProviderProps['basename'],
setSituationVersion: (updater: (situationVersion: number) => number) => void
) => {
const newWorker: Worker = new EngineWorker()
const promises: WorkerEngine['promises'] = []
const postMessage = async <T extends Actions['action'], U extends Action<T>>(
engineId: number,
action: T,
...params: U['params']
) => {
if (action === 'setSituation') {
setSituationVersion((situationVersion) => situationVersion + 1)
}
const warning = setTimeout(() => {
console.log('promise waiting for too long, aborting!', action, params)
promises[id].reject?.(new Error('timeout'))
}, 5000)
const id = promises.length
const promise = new Promise<U['result']>((resolve, reject) => {
promises[id] = {
resolve(...params: unknown[]) {
clearTimeout(warning)
return resolve(...(params as Parameters<typeof resolve>))
},
reject(err) {
clearTimeout(warning)
return reject(err)
},
}
})
newWorker.postMessage({ engineId, action, params, id })
return promise
}
newWorker.onmessage = function (e) {
console.log('msg:', e.data)
if ('error' in e.data) {
return promises[e.data.id].reject?.(e.data.error)
}
promises[e.data.id].resolve?.(e.data.result)
}
const isWorkerReady = postMessage(0, 'init', { basename })
const workerEngine = {
worker: newWorker,
postMessage,
isWorkerReady,
promises,
}
return workerEngine
}
const sleepMs = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))
let worker: ReturnType<typeof initWorkerEngine> | null = null
/**
* This hook is used to create a worker engine.
* @param basename
*/
const useCreateWorkerEngine = (basename: ProviderProps['basename']) => {
const [situationVersion, setSituationVersion] = useState(0)
useEffect(() => {
worker = initWorkerEngine(basename, setSituationVersion)
console.time('init')
void worker.isWorkerReady.then(() => console.timeEnd('init'))
// example of usage
void Promise.all([
asyncEvaluate('SMIC').then((result) => console.log('result', result)),
asyncEvaluate('date').then((result) => console.log('result', result)),
])
return () => {
console.log('worker terminated!')
worker?.worker.terminate()
worker?.promises.forEach((promise) =>
promise.reject?.('worker terminated')
)
worker = null
}
}, [basename])
return situationVersion
}
// asynchronous setSituation function
/**
* This function is used to set the situation in the worker with a specific engineId.
*/
export const asyncSetSituationWithEngineId = async (
engineId: number,
...params: Action<'setSituation'>['params']
): Promise<Action<'setSituation'>['result']> => {
if (!worker) {
await sleepMs(10)
return asyncSetSituationWithEngineId(engineId, ...params)
}
return await worker.postMessage(engineId, 'setSituation', ...params)
}
/**
* * This function is used to set the situation in the worker.
*/
export const asyncSetSituation = async (
...params: Action<'setSituation'>['params']
) => asyncSetSituationWithEngineId(0, ...params)
// asynchronous evaluate function
/**
* This function is used to evaluate a publicodes expression in the worker with a specific engineId.
*/
export const asyncEvaluateWithEngineId = async (
engineId: number,
...params: Action<'evaluate'>['params']
): Promise<Action<'evaluate'>['result']> => {
if (!worker) {
await sleepMs(10)
return asyncEvaluateWithEngineId(engineId, ...params)
}
return await worker.postMessage(engineId, 'evaluate', ...params)
}
/**
* This function is used to evaluate a publicodes expression in the worker.
*/
export const asyncEvaluate = async (...params: Action<'evaluate'>['params']) =>
asyncEvaluateWithEngineId(0, ...params)
/**
* This hook is used to evaluate a publicodes expression in the worker.
* @param defaultValue
*/
// export const useAsyncEvaluate = <T extends unknown = undefined>(
// defaultValue?: T
// ) => {
// const [response, setResponse] = useState<Action<'evaluate'>['result'] | T>(
// defaultValue as T
// )
// const evaluate = useCallback(async (value: PublicodesExpression) => {
// const result = await asyncEvaluate(value)
// setResponse(result)
// return result
// }, [])
// return [response, evaluate] as const
// }
// asynchronous getRule function:
/**
* This function is used to get a publicodes rule that is in the worker with a specific EngineId.
*/
export const asyncGetRuleWithEngineId = async (
engineId: number,
...params: Action<'getRule'>['params']
): Promise<Action<'getRule'>['result']> => {
if (!worker) {
await sleepMs(10)
return asyncGetRuleWithEngineId(engineId, ...params)
}
return await worker.postMessage(engineId, 'getRule', ...params)
}
/**
* This function is used to get a rule in the worker.
*/
export const asyncGetRule = async (...params: Action<'getRule'>['params']) =>
asyncGetRuleWithEngineId(0, ...params)
/**
* This hook is used to get a rule in the worker.
* @param defaultValue
*/
export const useAsyncGetRule = <
Names extends DottedName,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
T extends unknown = undefined
>(
dottedName: Names,
defaultValue?: T
) =>
usePromiseOnSituationChange(
() => asyncGetRule(dottedName),
[dottedName],
defaultValue
)
// asynchronous getParsedRules function
/**
* This function is used to get all the parsed rules in the worker with a specific engineId.
*/
export const asyncGetParsedRulesWithEngineId = async (
engineId: number
): Promise<Action<'getParsedRules'>['result']> => {
if (!worker) {
await sleepMs(10)
return asyncGetParsedRulesWithEngineId(engineId)
}
return await worker.postMessage(engineId, 'getParsedRules')
}
/**
* This function is used to get all the parsed rules in the worker.
*/
export const asyncGetParsedRules = async () =>
asyncGetParsedRulesWithEngineId(0)
/**
*
*/
export const useParsedRules = (engineId = 0) =>
usePromiseOnSituationChange(
async () => await asyncGetParsedRulesWithEngineId(engineId),
[engineId]
)
// asynchronous shallowCopy function
/**
* This function is used to shallow copy an engine in the worker with a specific engineId.
*/
export const asyncShallowCopyWithEngineId = async (
engineId: number
): Promise<Action<'shallowCopy'>['result']> => {
if (!worker) {
await sleepMs(10)
return asyncShallowCopyWithEngineId(engineId)
}
return await worker.postMessage(engineId, 'shallowCopy')
}
/**
* This function is used to shallow copy an engine in the worker.
*/
export const asyncShallowCopy = async () => asyncShallowCopyWithEngineId(0)
// asynchronous deleteShallowCopy function
/**
* * This function is used to delete a shallow copy of an engine in the worker.
*/
export const asyncDeleteShallowCopy = async (
engineId: number
): Promise<Action<'deleteShallowCopy'>['result']> => {
if (!worker) {
await sleepMs(10)
return asyncDeleteShallowCopy(engineId)
}
return await worker.postMessage(0, 'deleteShallowCopy', { engineId })
}
const SituationUpdated = React.createContext<number>(0)
export const SituationUpdatedProvider = ({
children,
basename,
}: {
children: React.ReactNode
basename: ProviderProps['basename']
}) => {
const situationVersion = useCreateWorkerEngine(basename)
return (
<SituationUpdated.Provider value={situationVersion}>
{children}
</SituationUpdated.Provider>
)
}
export const useSituationUpdated = () => {
const situationVersion = useContext(SituationUpdated)
return situationVersion
}
/**
* Wrapper around usePromise that adds the situation version to the dependencies
* @example const date = usePromiseOnSituationChange(() => asyncEvaluate('date'), []) // date will be updated when the situation changes
*/
export const usePromiseOnSituationChange = <T, Default = undefined>(
promise: () => Promise<T>,
deps: DependencyList,
defaultValue?: Default
): T | Default => {
const situationVersion = useSituationUpdated()
// eslint-disable-next-line react-hooks/exhaustive-deps
const state = usePromise(promise, [...deps, situationVersion], defaultValue)
return state
}

View File

@ -41,6 +41,17 @@ export default defineConfig(({ command, mode }) => ({
IS_STAGING: mode === 'production' && !isProductionBranch(mode),
IS_PRODUCTION: mode === 'production' && isProductionBranch(mode),
},
worker: {
plugins: [
yaml({
transform(data, filePath) {
return filePath.endsWith('/rules-en.yaml')
? cleanAutomaticTag(data)
: data
},
}),
],
},
plugins: [
{
name: 'run-script-on-file-change',