From 6bf97270813aa68ab73cc9ca849a5020f7aae969 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 7 Aug 2025 09:27:14 -0400 Subject: [PATCH 1/7] feat: add `pending` property to forms and commands --- .changeset/purple-hotels-prove.md | 5 ++ packages/kit/src/exports/public.d.ts | 12 ++- .../src/runtime/app/server/remote/command.js | 5 ++ .../kit/src/runtime/app/server/remote/form.js | 10 +++ .../client/remote-functions/command.js | 71 --------------- .../client/remote-functions/command.svelte.js | 87 +++++++++++++++++++ .../client/remote-functions/form.svelte.js | 16 ++++ .../runtime/client/remote-functions/index.js | 2 +- .../basics/src/routes/remote/+page.svelte | 5 ++ .../src/routes/remote/form/+page.svelte | 4 + 10 files changed, 143 insertions(+), 74 deletions(-) create mode 100644 .changeset/purple-hotels-prove.md delete mode 100644 packages/kit/src/runtime/client/remote-functions/command.js create mode 100644 packages/kit/src/runtime/client/remote-functions/command.svelte.js diff --git a/.changeset/purple-hotels-prove.md b/.changeset/purple-hotels-prove.md new file mode 100644 index 000000000000..dabdad0f3ef7 --- /dev/null +++ b/.changeset/purple-hotels-prove.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `pending` property to forms and commands diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 1982000eb2f9..c323bc0fd76d 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1565,6 +1565,8 @@ export type RemoteForm = { for(key: string | number | boolean): Omit, 'for'>; /** The result of the form submission */ get result(): Result | undefined; + /** The number of pending submissions */ + get pending(): number; /** Spread this onto a ` diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/form/+page.svelte index d6929d0f3fc3..2641c958e34a 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/form/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/remote/form/+page.svelte @@ -6,6 +6,10 @@

{#await current_task then task}{task}{/await}

+ +

Form pending: {task_one.pending}

+

Button pending: {task_two.buttonProps.pending}

+
From 99334f3cf8f24b9facdd52277d7d1a0dd1f3949f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 7 Aug 2025 09:30:07 -0400 Subject: [PATCH 2/7] reinstate comment --- .../kit/src/runtime/client/remote-functions/command.svelte.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/kit/src/runtime/client/remote-functions/command.svelte.js b/packages/kit/src/runtime/client/remote-functions/command.svelte.js index 1923eb7163f6..6c5698a369a6 100644 --- a/packages/kit/src/runtime/client/remote-functions/command.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/command.svelte.js @@ -17,6 +17,8 @@ export function command(id) { /** @type {number} */ let pending_count = $state(0); + // Careful: This function MUST be synchronous (can't use the async keyword) because the return type has to be a promise with an updates() method. + // If we make it async, the return type will be a promise that resolves to a promise with an updates() method, which is not what we want. /** @type {RemoteCommand} */ const command_function = (arg) => { /** @type {Array | RemoteQueryOverride>} */ From aa451e37ff068a607a34cce0c9bdc3cc0bfd3876 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 7 Aug 2025 10:29:21 -0400 Subject: [PATCH 3/7] tests --- .../basics/src/routes/remote/+page.svelte | 12 +++- .../src/routes/remote/form/+page.svelte | 6 +- .../src/routes/remote/form/form.remote.js | 21 +++++- .../src/routes/remote/query-command.remote.js | 16 ++++- .../kit/test/apps/basics/test/client.test.js | 68 +++++++++++++++++++ 5 files changed, 117 insertions(+), 6 deletions(-) diff --git a/packages/kit/test/apps/basics/src/routes/remote/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/+page.svelte index 464d9dd46e37..7ff8d760bb61 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/remote/+page.svelte @@ -1,7 +1,7 @@ @@ -55,6 +55,10 @@

{task_one.result}

{task_two.result}

+ + + + {#each ['foo', 'bar'] as item}
{task_one.for(item).result} diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/form.remote.js b/packages/kit/test/apps/basics/src/routes/remote/form/form.remote.js index 213b525b6880..f5bb0699758d 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/form/form.remote.js +++ b/packages/kit/test/apps/basics/src/routes/remote/form/form.remote.js @@ -2,11 +2,20 @@ import { form, query } from '$app/server'; import { error, redirect } from '@sveltejs/kit'; let task; +const deferreds = []; export const get_task = query(() => { return task; }); +export const resolve_deferreds = form(async () => { + for (const deferred of deferreds) { + deferred.resolve(); + } + deferreds.length = 0; // Clear the array + return 'resolved'; +}); + export const task_one = form(async (form_data) => { task = /** @type {string} */ (form_data.get('task')); @@ -16,7 +25,11 @@ export const task_one = form(async (form_data) => { if (task === 'redirect') { redirect(303, '/remote'); } - if (task === 'override') { + if (task === 'deferred') { + const deferred = Promise.withResolvers(); + deferreds.push(deferred); + await deferred.promise; + } else if (task === 'override') { await new Promise((resolve) => setTimeout(resolve, 500)); } @@ -29,7 +42,11 @@ export const task_two = form(async (form_data) => { if (task === 'error') { throw new Error('Unexpected error'); } - if (task === 'override') { + if (task === 'deferred') { + const deferred = Promise.withResolvers(); + deferreds.push(deferred); + await deferred.promise; + } else if (task === 'override') { await new Promise((resolve) => setTimeout(resolve, 500)); } diff --git a/packages/kit/test/apps/basics/src/routes/remote/query-command.remote.js b/packages/kit/test/apps/basics/src/routes/remote/query-command.remote.js index f1eb258b84e0..147bc6c6eb0e 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/query-command.remote.js +++ b/packages/kit/test/apps/basics/src/routes/remote/query-command.remote.js @@ -4,16 +4,28 @@ export const echo = query('unchecked', (value) => value); export const add = query('unchecked', ({ a, b }) => a + b); let count = 0; +const deferreds = []; export const get_count = query(() => count); -export const set_count = command('unchecked', async ({ c, slow = false }) => { - if (slow) { +export const set_count = command('unchecked', async ({ c, slow = false, deferred = false }) => { + if (deferred) { + const deferred = Promise.withResolvers(); + deferreds.push(deferred); + await deferred.promise; + } else if (slow) { await new Promise((resolve) => setTimeout(resolve, 500)); } return (count = c); }); +export const resolve_deferreds = command(() => { + for (const deferred of deferreds) { + deferred.resolve(); + } + deferreds.length = 0; // Clear the array +}); + export const set_count_server = command('unchecked', async (c) => { count = c; await get_count().refresh(); diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 1c4db540aef4..d55d7f860447 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -1815,6 +1815,25 @@ test.describe('remote functions', () => { expect(request_count).toBe(2); }); + test('command tracks pending state', async ({ page }) => { + await page.goto('/remote'); + + // Initial pending should be 0 + await expect(page.locator('#command-pending')).toHaveText('Command pending: 0'); + + // Start a slow command - this will hang until we resolve it + await page.click('#command-deferred-btn'); + + // Check that pending has incremented to 1 + await expect(page.locator('#command-pending')).toHaveText('Command pending: 1'); + + // Resolve the deferred command + await page.click('#resolve-deferreds'); + + // Wait for the command to complete and pending to go back to 0 + await expect(page.locator('#command-pending')).toHaveText('Command pending: 0'); + }); + test('validation works', async ({ page }) => { await page.goto('/remote/validation'); await expect(page.locator('p')).toHaveText('pending'); @@ -1834,4 +1853,53 @@ test.describe('remote functions', () => { await page.click('button:nth-of-type(4)'); await expect(page.locator('p')).toHaveText('success'); }); + + test('command pending state is tracked correctly', async ({ page }) => { + await page.goto('/remote'); + + // Initially no pending commands + await expect(page.locator('#command-pending')).toHaveText('Command pending: 0'); + + // Start a slow command - this will hang until we resolve it + await page.click('#command-deferred-btn'); + + // Check that pending has incremented to 1 + await expect(page.locator('#command-pending')).toHaveText('Command pending: 1'); + + // Resolve the deferred command + await page.click('#resolve-deferreds'); + + // Wait for the command to complete and verify results + await expect(page.locator('#command-result')).toHaveText('7'); + + // Verify pending count returns to 0 + await expect(page.locator('#command-pending')).toHaveText('Command pending: 0'); + }); + + test('form pending state is tracked correctly', async ({ page }) => { + await page.goto('/remote/form'); + + // Initially no pending forms + await expect(page.locator('#form-pending')).toHaveText('Form pending: 0'); + await expect(page.locator('#form-button-pending')).toHaveText('Button pending: 0'); + + // Fill form with slow operation + await page.fill('#input-task', 'deferred'); + + // Submit form - this will hang until we resolve it + await page.click('#submit-btn-one'); + + // Check that pending has incremented to 1 + await expect(page.locator('#form-pending')).toHaveText('Form pending: 1'); + + // Resolve the deferred form submission + await page.click('#resolve-deferreds'); + + // Wait for form submission to complete and verify results + await expect(page.locator('#get-task')).toHaveText('deferred'); + + // Verify pending count returns to 0 + await expect(page.locator('#form-pending')).toHaveText('Form pending: 0'); + await expect(page.locator('#form-button-pending')).toHaveText('Button pending: 0'); + }); }); From a428ee3b335c43d95f1bdd01431900d257cb3992 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 7 Aug 2025 12:05:58 -0400 Subject: [PATCH 4/7] prettier --- .../basics/src/routes/remote/+page.svelte | 8 +++++- .../kit/test/apps/basics/test/client.test.js | 26 +++++++++---------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/kit/test/apps/basics/src/routes/remote/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/+page.svelte index 7ff8d760bb61..64e3183ac235 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/remote/+page.svelte @@ -1,7 +1,13 @@