statelyai / xstate-tools

Public monorepo for XState tooling
183 stars 40 forks source link

vscode extension sometimes adds multiple annotations to machine #274

Open viglucci opened 1 year ago

viglucci commented 1 year ago

Description

The VSCode extension sometimes adds multiple annotations to a machine definition. I'm not entirely sure what behavior or steps I am taking that might be causing this to happen.

Each added annotation has different data and is a different length.

image

Environment

XState VSCode version: v1.11.0

JoepKockelkorn commented 1 year ago

I have this same issue, also not sure what causes this.

Andarist commented 1 year ago

If you could come up with repro steps - that would be immensely helpful in fixing this.

davidkpiano commented 1 year ago

I have a hunch that it has to do with the indentation of the first /** @xstate-layout which makes the extension fail to recognize that there is already a layout string? Wild guess though

Andarist commented 1 year ago

I think that it's likely that you had some other error before this happened, something failed to sync properly and impacted future sync - causing the duplicate layout strings to be inserted.

Andarist commented 1 year ago

@viglucci @JoepKockelkorn could you try the newest version of the VS Code extension? Does the problem persist?

JoepKockelkorn commented 1 year ago

I just tried on version v1.11.2 (xstate version 4.33.6) but eventually it still failed. What I did was the following:

image

So there are actually two bugs happening at the same time:

I see these errors coming by in the Extension Host logs:

2022-12-16 10:16:03.710 [error] Error: Request applyMachineEdits failed with message: Unexpected token, expected "," (35:6)
    at handleResponse (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329487:40)
    at processMessageQueue (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329325:13)
    at Immediate.<anonymous> (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329311:11)
    at process.processImmediate (node:internal/timers:466:21)
2022-12-16 10:16:06.592 [error] Error: Request applyMachineEdits failed with message: Could not find state
    at handleResponse (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329487:40)
    at processMessageQueue (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329325:13)
    at Immediate.<anonymous> (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329311:11)
    at process.processImmediate (node:internal/timers:466:21)
2022-12-16 10:16:21.168 [error] Error: Request applyMachineEdits failed with message: Unterminated string constant. (32:11)
    at handleResponse (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329487:40)
    at processMessageQueue (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329325:13)
    at Immediate.<anonymous> (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329311:11)
    at process.processImmediate (node:internal/timers:466:21)
2022-12-16 10:16:43.921 [error] Error: Request applyMachineEdits failed with message: Unexpected token, expected "," (35:6)
    at handleResponse (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329487:40)
    at processMessageQueue (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329325:13)
    at Immediate.<anonymous> (/Users/joepkockelkorn/.vscode/extensions/statelyai.stately-vscode-1.11.2/dist/index.js:329311:11)
    at process.processImmediate (node:internal/timers:466:21)

Here is my machine code (imports + typings omitted):

export const signUpMachine = createMachine({
    id: 'SignUp',
    tsTypes: {} as import('./sign-up.machine.typegen').Typegen0,
    initial: 'selectPriceplan',
    states: {
        registered: {
            entry: 'cleanLocalStorage',
            type: 'final',
            meta: { step: 4 },
        },
        checkingPaymentStatus: {
            after: {
                '60000': {
                    target: 'checkPaymentFailed',
                    actions: [],
                    internal: false,
                },
            },
            initial: 'loading',
            states: {
                loading: {
                    invoke: {
                        src: 'checkStatus',
                        onDone: [
                            {
                                target: '#SignUp.checkingRegistrationStatus',
                                cond: 'paid',
                                actions: ['saveStatus', 'resetRetryAttempts'],
                            },
                            {
                                target: 'waiting',
                                actions: 'incrementAttempts',
                            },
                        ],
                        onError: [
                            {
                                target: '#SignUp.workflowNotFound',
                                cond: 'not found',
                            },
                            {
                                target: '#SignUp.checkPaymentFailed',
                            },
                        ],
                    },
                },
                waiting: {
                    after: {
                        DELAY_WITH_BACKOFF: {
                            target: '#SignUp.checkingPaymentStatus.loading',
                            actions: [],
                            internal: false,
                        },
                    },
                },
            },
            meta: { step: 4, loading: true },
        },
        checkPaymentFailed: {
            type: 'final',
            meta: { step: 4 },
        },
        loadingCheckoutUrl: {
            invoke: {
                src: 'loadCheckoutUrl',
                onDone: [
                    {
                        target: 'loadCheckoutUrlSuccess',
                        actions: 'saveCheckoutUrl',
                    },
                ],
                onError: [
                    {
                        target: 'loadCheckoutUrlFailed',
                    },
                ],
            },
            meta: { step: 3, loading: true },
        },
        loadCheckoutUrlSuccess: {
            type: 'final',
            meta: { step: 3 },
        },
        loadCheckoutUrlFailed: {
            type: 'final',
            meta: { step: 3 },
        },
        enterUserRegistration: {
            description: "If existingUser then don't show password fields and terms.",
            always: {
                target: 'enterEmailCodes',
                cond: 'registrationRequested',
            },
            on: {
                'submit.userRegistration': {
                    target: 'checkingUserRegistration',
                    actions: 'storeEnteredPersonalDetails',
                },
            },
            meta: { step: 1 },
        },
        enterEmailCodes: {
            always: [
                {
                    target: 'checkingRegistrationStatus',
                    cond: 'emailAddressesVerified and (payment not required or paid)',
                },
                {
                    target: 'checkingPaymentStatus',
                    cond: 'emailAddressesVerified, paymentRequired and checkout done but not paid',
                    actions: 'resetRetryAttempts',
                },
                {
                    target: 'loadingCheckoutUrl',
                    cond: 'emailAddressVerified, paymentRequired but not checkout done and not paid',
                },
            ],
            on: {
                'submit.emailCodes': {
                    target: 'checkingEmailCodes',
                },
            },
            meta: { step: 2 },
        },
        checkingEmailCodes: {
            invoke: {
                src: 'checkEmailCodes',
                onDone: [
                    {
                        target: 'enterEmailCodes',
                        cond: 'codes invalid',
                        actions: 'saveVerifyEmailCodeResponse',
                    },
                    {
                        target: 'loadingCheckoutUrl',
                        cond: 'payment required',
                    },
                    {
                        target: 'checkingRegistrationStatus',
                    },
                ],
                onError: [
                    {
                        target: 'enterEmailCodes',
                        actions: 'showErrorInDialog',
                    },
                ],
            },
            meta: { step: 2, loading: true },
        },
        checkingUserRegistration: {
            invoke: {
                src: 'checkUserRegistration',
                onDone: [
                    {
                        target: 'enterEmailCodes',
                        actions: 'storeRegisterCustomerResponse',
                    },
                ],
                onError: [
                    {
                        target: 'enterUserRegistration',
                        actions: 'showErrorInDialog',
                    },
                ],
            },
            meta: { step: 1, loading: true },
        },
        selectPriceplan: {
            always: [
                {
                    target: 'enterUserRegistration',
                    cond: 'priceplan selected',
                },
                {
                    target: 'enterUserRegistration',
                    cond: 'only one free priceplan available',
                    actions: 'preselectPriceplan',
                },
            ],
            on: {
                'select.pricePlan': {
                    target: 'enterUserRegistration',
                    actions: 'storePriceplan',
                },
            },
        },
        checkingRegistrationStatus: {
            after: {
                '60000': {
                    target: 'checkRegistrationFailed',
                    actions: [],
                    internal: false,
                },
            },
            initial: 'loading',
            states: {
                loading: {
                    invoke: {
                        src: 'checkStatus',
                        onDone: [
                            {
                                target: 'waiting',
                                actions: ['saveStatus', 'incrementAttempts'],
                            },
                        ],
                        onError: [
                            {
                                target: '#SignUp.workflowNotFound',
                                cond: 'not found',
                            },
                            {
                                target: '#SignUp.checkRegistrationFailed',
                            },
                        ],
                    },
                },
                waiting: {
                    after: {
                        DELAY_WITH_BACKOFF: {
                            target: '#SignUp.checkingRegistrationStatus.loading',
                            actions: [],
                            internal: false,
                        },
                    },
                },
            },
            meta: { step: 4, loading: true },
        },
        checkRegistrationFailed: {
            type: 'final',
            meta: { step: 4 },
        },
        workflowNotFound: {
            entry: ['cleanLocalStorage', 'redirectToSignUpOops'],
            type: 'final',
        },
    },
    always: [
        {
            target: 'registered',
            cond: 'registered',
        },
    ],
    schema: {
        context: {} as SignUpContext,
        events: {} as
            | { type: 'startPayment' }
            | { type: 'submit.emailCodes'; request: VerifyEmailCodeRequest }
            | { type: 'submit.userRegistration'; request: RegisterCustomerRequest }
            | { type: 'select.pricePlan'; price: Price },
        services: {} as {
            checkStatus: { data: CheckStatusResponse };
            checkUserRegistration: { data: RegisterCustomerResponse };
            checkEmailCodes: { data: VerifyEmailCodeResponse };
            loadCheckoutUrl: { data: GetCheckoutUrlResponse };
        },
    },
    context: { checkoutComplete: false },
    predictableActionArguments: true,
    preserveActionOrder: true,
});
Andarist commented 1 year ago

@JoepKockelkorn thank you for all of this detail! However, I fail to reproduce this with the given steps. The state is being added correctly - maybe it depends on where we add it (?). I'm also a little bit confused by one of the last steps in which you remove a new state. If you added a new state, it didn't pop up in the code, and then if you reopened the Visual Editor... there should be a new state there (since it didn't get synced to the code before).

JoepKockelkorn commented 1 year ago

Yes, indeed the new state was only in the visual, not in the code. So that's where I removed it. As soon as I removed it, the second metadata comment was created. The way I added the new state was double clicking anywhere in the machine. So there wasn't any way to get in that state, but it should have been created anyway, right?

But I think it doesn't matter what change I make to the machine, it fails in updating the machine right away. This is using the exact same machine code as above:

bug

andrejpavlovic commented 1 year ago

This happens pretty frequently for me. The larger and more complicated the state gets, the more likely that some change will cause a duplication of @xstate-layout issue. If I don't notice it right away, any changes I make to the layout will essentially not be saved. I've started using the VS Code extension a few days ago, and I'd say I encounter this issue constantly - every 10-15 minutes, across various statecharts while working on them.

Some error messages I've seen outputted from the extension that cause this issue:

2023-01-04 15:21:28.077 [error] Error: Request applyMachineEdits failed with message: Array element was not an object expression
    at handleResponse (c:\Users\Andrej\.vscode\extensions\statelyai.stately-vscode-1.11.3\dist\index.js:329487:40)
    at processMessageQueue (c:\Users\Andrej\.vscode\extensions\statelyai.stately-vscode-1.11.3\dist\index.js:329325:13)
    at Immediate.<anonymous> (c:\Users\Andrej\.vscode\extensions\statelyai.stately-vscode-1.11.3\dist\index.js:329311:11)
    at process.processImmediate (node:internal/timers:466:21)
2023-01-04 15:23:19.401 [error] Error: Request applyMachineEdits failed with message: Unexpected token, expected "," (37:1)
    at handleResponse (c:\Users\Andrej\.vscode\extensions\statelyai.stately-vscode-1.11.3\dist\index.js:329487:40)
    at processMessageQueue (c:\Users\Andrej\.vscode\extensions\statelyai.stately-vscode-1.11.3\dist\index.js:329325:13)
    at Immediate.<anonymous> (c:\Users\Andrej\.vscode\extensions\statelyai.stately-vscode-1.11.3\dist\index.js:329311:11)
    at process.processImmediate (node:internal/timers:466:21)
2023-01-04 15:23:19.449 [error] Error: Request applyMachineEdits failed with message: Changing transition path requires the transitionPath ([on, asd, 0]) to exist on the source state ([s2])
    at handleResponse (c:\Users\Andrej\.vscode\extensions\statelyai.stately-vscode-1.11.3\dist\index.js:329487:40)
    at processMessageQueue (c:\Users\Andrej\.vscode\extensions\statelyai.stately-vscode-1.11.3\dist\index.js:329325:13)
    at Immediate.<anonymous> (c:\Users\Andrej\.vscode\extensions\statelyai.stately-vscode-1.11.3\dist\index.js:329311:11)
    at process.processImmediate (node:internal/timers:466:21)
rjchow commented 1 year ago

Similarly I have issues with the editor syncing, to the point where it makes it untenable for me to introduce xstate to my team.

If there's an error with syncing, the editor should fail fast and obvious so the user does not continue inputting work that cannot be recovered.

davidkpiano commented 1 year ago

Similarly I have issues with the editor syncing, to the point where it makes it untenable for me to introduce xstate to my team.

If there's an error with syncing, the editor should fail fast and obvious so the user does not continue inputting work that cannot be recovered.

Thanks for the feedback, I agree. We're prioritizing the syncing issues with the extension (not the easiest problem to solve 😅)