From c96b1e68cd3d2cae83e7b06422604ff605872f8b Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Fri, 9 Oct 2026 14:55:00 -0600 Subject: [PATCH] fix: only allow each key to be set once in a server `Warp` --- documentation/docs/06-runtime/05-warp.md | 2 +- .../98-reference/.generated/server-errors.md | 8 ++++++++ .../svelte/messages/server-errors/errors.md | 6 ++++++ packages/svelte/src/internal/server/errors.js | 15 +++++++++++++++ packages/svelte/src/internal/server/warp.js | 12 +++++++++++- .../svelte/src/internal/server/warp.test.ts | 19 +++++++++++++++++++ 6 files changed, 60 insertions(+), 2 deletions(-) diff --git a/documentation/docs/06-runtime/05-warp.md b/documentation/docs/06-runtime/05-warp.md index 6a6efa7efc..7b919ff24c 100644 --- a/documentation/docs/06-runtime/05-warp.md +++ b/documentation/docs/06-runtime/05-warp.md @@ -45,7 +45,7 @@ const warp = new Warp('my-app'); const rand = warp.getOrInsertComputed('random', () => Math.random()); ``` -On the client, a `Warp` behaves like any other `Map` — values you add to it stay there until you remove them with `delete` or `clear`. On the server, values can't be removed, since they may already be in use or on their way to the client. +On the client, a `Warp` behaves like any other `Map` — values you add to it stay there until you remove them with `delete` or `clear`. On the server, each key can only be set once, and values can't be removed, since they may already be in use or on their way to the client. For the same reason, treat values as immutable once they've been added to a `Warp`. Keys can be strings, numbers, booleans or bigints. If you're a library author, use your package name as the `Warp`'s id, so that it doesn't conflict with other libraries: diff --git a/documentation/docs/98-reference/.generated/server-errors.md b/documentation/docs/98-reference/.generated/server-errors.md index b14a512c04..6213838eba 100644 --- a/documentation/docs/98-reference/.generated/server-errors.md +++ b/documentation/docs/98-reference/.generated/server-errors.md @@ -94,6 +94,14 @@ Each `withWarp` call can only contain one `render`, since all the values added t `withWarp(...)` cannot be called inside another `withWarp(...)` or `render(...)` call ``` +### warp_key_exists + +``` +Key `%key%` has already been set in `Warp` `%id%`%stack% +``` + +On the server, each key in a `Warp` can only be set once. Something may already have read the first value during the render, so replacing it would mean the client hydrates with different data than the server rendered with. Use `getOrInsert` or `getOrInsertComputed` if the value might already exist. + ### warp_method_unsupported ``` diff --git a/packages/svelte/messages/server-errors/errors.md b/packages/svelte/messages/server-errors/errors.md index 133546da10..6ce64ef2ff 100644 --- a/packages/svelte/messages/server-errors/errors.md +++ b/packages/svelte/messages/server-errors/errors.md @@ -70,6 +70,12 @@ Each `withWarp` call can only contain one `render`, since all the values added t > `withWarp(...)` cannot be called inside another `withWarp(...)` or `render(...)` call +## warp_key_exists + +> Key `%key%` has already been set in `Warp` `%id%`%stack% + +On the server, each key in a `Warp` can only be set once. Something may already have read the first value during the render, so replacing it would mean the client hydrates with different data than the server rendered with. Use `getOrInsert` or `getOrInsertComputed` if the value might already exist. + ## warp_method_unsupported > `warp.%method%(...)` is not supported on the server diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js index a63876f87c..8fedbf6b4c 100644 --- a/packages/svelte/src/internal/server/errors.js +++ b/packages/svelte/src/internal/server/errors.js @@ -142,6 +142,21 @@ export function warp_context_nested() { throw error; } +/** + * Key `%key%` has already been set in `Warp` `%id%`%stack% + * @param {string} key + * @param {string} id + * @param {string} stack + * @returns {never} + */ +export function warp_key_exists(key, id, stack) { + const error = new Error(`warp_key_exists\nKey \`${key}\` has already been set in \`Warp\` \`${id}\`${stack}\nhttps://svelte.dev/e/warp_key_exists`); + + error.name = 'Svelte error'; + + throw error; +} + /** * `warp.%method%(...)` is not supported on the server * @param {string} method diff --git a/packages/svelte/src/internal/server/warp.js b/packages/svelte/src/internal/server/warp.js index 8b5975dbc7..47c00ac1cb 100644 --- a/packages/svelte/src/internal/server/warp.js +++ b/packages/svelte/src/internal/server/warp.js @@ -58,7 +58,17 @@ export class Warp { * @returns {this} */ set(key, value) { - set(get_store(), this.#id, key, value); + const store = get_store(); + + if (get_values(store, this.#id).has(key)) { + e.warp_key_exists( + String(key), + this.#id, + DEV ? `. It was first set in:\n${get_stack(store, this.#id, key)}` : '' + ); + } + + set(store, this.#id, key, value); return this; } diff --git a/packages/svelte/src/internal/server/warp.test.ts b/packages/svelte/src/internal/server/warp.test.ts index 52624571fc..a2f67ef2c4 100644 --- a/packages/svelte/src/internal/server/warp.test.ts +++ b/packages/svelte/src/internal/server/warp.test.ts @@ -276,6 +276,25 @@ describe('withWarp', () => { expect(values?.get('a')).toBe('render'); expect(values?.get('b')).toBe('withWarp'); }); + + test('throws when a key is set twice', async () => { + await expect( + render(() => { + warp.set('a', 1); + warp.set('a', 2); + }) + ).rejects.toThrow('warp_key_exists'); + }); + + test('getOrInsert does not throw when the key exists', async () => { + const { head } = await render(() => { + warp.set('a', 1); + expect(warp.getOrInsert('a', 2)).toBe(1); + expect(warp.getOrInsertComputed('a', () => 3)).toBe(1); + }); + + expect(revive(head)?.get('test')?.get('a')).toBe(1); + }); }); describe('hydratable', () => {