From b0b4ed56ab6c226529526917e15b979ff45d2fcf Mon Sep 17 00:00:00 2001
From: Maxim Khramtsov <khraks.mamtsov@gmail.com>
Date: Fri, 20 Dec 2024 15:43:48 +0100
Subject: [PATCH 1/9] add Url module

---
 .changeset/thin-coats-bake.md      |   8 ++
 packages/platform/src/Error.ts     |   2 +-
 packages/platform/src/Url.ts       | 150 +++++++++++++++++++++++++++++
 packages/platform/src/index.ts     |   5 +
 packages/platform/test/Url.test.ts |  28 ++++++
 5 files changed, 192 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/thin-coats-bake.md
 create mode 100644 packages/platform/src/Url.ts
 create mode 100644 packages/platform/test/Url.test.ts

diff --git a/.changeset/thin-coats-bake.md b/.changeset/thin-coats-bake.md
new file mode 100644
index 00000000000..4e24c68f6b7
--- /dev/null
+++ b/.changeset/thin-coats-bake.md
@@ -0,0 +1,8 @@
+---
+"@effect/platform": patch
+---
+
+`Url` module has been introduced:
+
+- immutable setters with dual-function api
+- integration with `UrlParams`
diff --git a/packages/platform/src/Error.ts b/packages/platform/src/Error.ts
index d1354823527..d042b7cf08e 100644
--- a/packages/platform/src/Error.ts
+++ b/packages/platform/src/Error.ts
@@ -65,7 +65,7 @@ export declare namespace PlatformError {
   export interface Base {
     readonly [PlatformErrorTypeId]: typeof PlatformErrorTypeId
     readonly _tag: string
-    readonly module: "Clipboard" | "Command" | "FileSystem" | "KeyValueStore" | "Path" | "Stream" | "Terminal"
+    readonly module: "Clipboard" | "Command" | "FileSystem" | "KeyValueStore" | "Path" | "Stream" | "Terminal" | "Url"
     readonly method: string
     readonly message: string
   }
diff --git a/packages/platform/src/Url.ts b/packages/platform/src/Url.ts
new file mode 100644
index 00000000000..6a43f606c34
--- /dev/null
+++ b/packages/platform/src/Url.ts
@@ -0,0 +1,150 @@
+/**
+ * @since 1.0.0
+ */
+import * as Either from "effect/Either"
+import { dual } from "effect/Function"
+import * as Error from "./Error.js"
+import * as UrlParams from "./UrlParams.js"
+
+/** @internal */
+const immutableSetter = <M>(clone: (mutable: M) => M) =>
+<P extends keyof M>(property: P): {
+  (value: M[P]): (mutable: M) => M
+  (mutable: M, value: M[P]): M
+} =>
+  dual(2, (mutable: M, value: M[P]) => {
+    const result = clone(mutable)
+    result[property] = value
+    return result
+  })
+/** @internal */
+const immutableURLSetter = immutableSetter<URL>((url) => new URL(url))
+/**
+ * @since 1.0.0
+ * @category constructors
+ */
+export const make: {
+  (url: string | URL, base?: string | URL | undefined): Either.Either<URL, Error.BadArgument>
+} = (url, base) =>
+  Either.try({
+    try: () => new URL(url, base),
+    catch: (cause) =>
+      Error.BadArgument({
+        module: "Url",
+        method: "make",
+        message: cause instanceof globalThis.Error ? cause.message : "Invalid input"
+      })
+  })
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setHash: {
+  (hash: string): (url: URL) => URL
+  (url: URL, hash: string): URL
+} = immutableURLSetter("hash")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setHost: {
+  (host: string): (url: URL) => URL
+  (url: URL, host: string): URL
+} = immutableURLSetter("host")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setHostname: {
+  (hostname: string): (url: URL) => URL
+  (url: URL, hostname: string): URL
+} = immutableURLSetter("hostname")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setHref: {
+  (href: string): (url: URL) => URL
+  (url: URL, href: string): URL
+} = immutableURLSetter("href")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setPassword: {
+  (password: string): (url: URL) => URL
+  (url: URL, password: string): URL
+} = immutableURLSetter("password")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setPathname: {
+  (pathname: string): (url: URL) => URL
+  (url: URL, pathname: string): URL
+} = immutableURLSetter("pathname")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setPort: {
+  (port: string): (url: URL) => URL
+  (url: URL, port: string): URL
+} = immutableURLSetter("port")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setProtocol: {
+  (protocol: string): (url: URL) => URL
+  (url: URL, protocol: string): URL
+} = immutableURLSetter("protocol")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setSearch: {
+  (search: string): (url: URL) => URL
+  (url: URL, search: string): URL
+} = immutableURLSetter("search")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setUsername: {
+  (username: string): (url: URL) => URL
+  (url: URL, username: string): URL
+} = immutableURLSetter("username")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setUrlParams: {
+  (searchParams: UrlParams.UrlParams): (url: URL) => URL
+  (url: URL, username: UrlParams.UrlParams): URL
+} = dual(2, (url: URL, searchParams: UrlParams.UrlParams) => {
+  const result = new URL(url)
+  result.search = UrlParams.toString(searchParams)
+  return result
+})
+/**
+ * @since 1.0.0
+ * @category getters
+ */
+export const urlParams: {
+  (url: URL): UrlParams.UrlParams
+} = (url) => UrlParams.fromInput(url.searchParams)
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const modifyUrlParams: {
+  (f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): (url: URL) => URL
+  (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): URL
+} = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams) => {
+  const urlParams = UrlParams.fromInput(url.searchParams)
+  const newUrlParams = f(urlParams)
+  const result = new URL(url)
+  result.search = UrlParams.toString(newUrlParams)
+  return result
+})
diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts
index bf627d75004..e0a8523001e 100644
--- a/packages/platform/src/index.ts
+++ b/packages/platform/src/index.ts
@@ -249,6 +249,11 @@ export * as Terminal from "./Terminal.js"
  */
 export * as Transferable from "./Transferable.js"
 
+/**
+ * @since 1.0.0
+ */
+export * as Url from "./Url.js"
+
 /**
  * @since 1.0.0
  */
diff --git a/packages/platform/test/Url.test.ts b/packages/platform/test/Url.test.ts
new file mode 100644
index 00000000000..c9abb7426f2
--- /dev/null
+++ b/packages/platform/test/Url.test.ts
@@ -0,0 +1,28 @@
+import * as Url from "@effect/platform/Url"
+import * as UrlParams from "@effect/platform/UrlParams"
+import { assert, describe, it } from "@effect/vitest"
+import { Effect } from "effect"
+
+describe("Url", () => {
+  const testURL = new URL("https://example.com/test")
+
+  describe("make", () => {
+    it.effect("immutable", () =>
+      Effect.gen(function*() {
+        const url = yield* Url.make(testURL)
+        assert.notStrictEqual(url, testURL)
+      }))
+  })
+  describe("setters", () => {
+    it("immutable", () => {
+      const hashUrl = Url.setHash(testURL, "test")
+      assert.notStrictEqual(hashUrl, testURL)
+      assert.strictEqual(hashUrl.toString(), "https://example.com/test#test")
+    })
+  })
+  it("modifyUrlParams", () => {
+    const paramsUrl = Url.modifyUrlParams(testURL, (x) => UrlParams.append(x, "key", "value"))
+    assert.notStrictEqual(paramsUrl, testURL)
+    assert.strictEqual(paramsUrl.toString(), "https://example.com/test?key=value")
+  })
+})

From 367c55382b8ff7e758b1a963e843c731a8c00b52 Mon Sep 17 00:00:00 2001
From: Maxim Khramtsov <khraks.mamtsov@gmail.com>
Date: Sun, 22 Dec 2024 10:04:59 +0100
Subject: [PATCH 2/9] rename arg

---
 packages/platform/src/Url.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/platform/src/Url.ts b/packages/platform/src/Url.ts
index 6a43f606c34..bd144754120 100644
--- a/packages/platform/src/Url.ts
+++ b/packages/platform/src/Url.ts
@@ -120,8 +120,8 @@ export const setUsername: {
  * @category utils
  */
 export const setUrlParams: {
-  (searchParams: UrlParams.UrlParams): (url: URL) => URL
-  (url: URL, username: UrlParams.UrlParams): URL
+  (urlParams: UrlParams.UrlParams): (url: URL) => URL
+  (url: URL, urlParams: UrlParams.UrlParams): URL
 } = dual(2, (url: URL, searchParams: UrlParams.UrlParams) => {
   const result = new URL(url)
   result.search = UrlParams.toString(searchParams)

From 1b25b407491b555b22824182c843612c1ce4ce93 Mon Sep 17 00:00:00 2001
From: Maxim Khramtsov <khraks.mamtsov@gmail.com>
Date: Mon, 23 Dec 2024 08:13:59 +0100
Subject: [PATCH 3/9] PR fixes

---
 packages/platform/src/Error.ts |  2 +-
 packages/platform/src/Url.ts   | 46 +++++++++++++++++++++-------------
 2 files changed, 29 insertions(+), 19 deletions(-)

diff --git a/packages/platform/src/Error.ts b/packages/platform/src/Error.ts
index d042b7cf08e..d1354823527 100644
--- a/packages/platform/src/Error.ts
+++ b/packages/platform/src/Error.ts
@@ -65,7 +65,7 @@ export declare namespace PlatformError {
   export interface Base {
     readonly [PlatformErrorTypeId]: typeof PlatformErrorTypeId
     readonly _tag: string
-    readonly module: "Clipboard" | "Command" | "FileSystem" | "KeyValueStore" | "Path" | "Stream" | "Terminal" | "Url"
+    readonly module: "Clipboard" | "Command" | "FileSystem" | "KeyValueStore" | "Path" | "Stream" | "Terminal"
     readonly method: string
     readonly message: string
   }
diff --git a/packages/platform/src/Url.ts b/packages/platform/src/Url.ts
index bd144754120..c7c52ff02d6 100644
--- a/packages/platform/src/Url.ts
+++ b/packages/platform/src/Url.ts
@@ -1,9 +1,9 @@
 /**
  * @since 1.0.0
  */
+import * as Cause from "effect/Cause"
 import * as Either from "effect/Either"
 import { dual } from "effect/Function"
-import * as Error from "./Error.js"
 import * as UrlParams from "./UrlParams.js"
 
 /** @internal */
@@ -17,43 +17,44 @@ const immutableSetter = <M>(clone: (mutable: M) => M) =>
     result[property] = value
     return result
   })
+
 /** @internal */
 const immutableURLSetter = immutableSetter<URL>((url) => new URL(url))
+
 /**
  * @since 1.0.0
  * @category constructors
  */
 export const make: {
-  (url: string | URL, base?: string | URL | undefined): Either.Either<URL, Error.BadArgument>
+  (url: string | URL, base?: string | URL | undefined): Either.Either<URL, Cause.IllegalArgumentException>
 } = (url, base) =>
   Either.try({
     try: () => new URL(url, base),
     catch: (cause) =>
-      Error.BadArgument({
-        module: "Url",
-        method: "make",
-        message: cause instanceof globalThis.Error ? cause.message : "Invalid input"
-      })
+      new Cause.IllegalArgumentException(cause instanceof globalThis.Error ? cause.message : "Invalid input")
   })
+
 /**
  * @since 1.0.0
- * @category utils
+ * @category setters
  */
 export const setHash: {
   (hash: string): (url: URL) => URL
   (url: URL, hash: string): URL
 } = immutableURLSetter("hash")
+
 /**
  * @since 1.0.0
- * @category utils
+ * @category setters
  */
 export const setHost: {
   (host: string): (url: URL) => URL
   (url: URL, host: string): URL
 } = immutableURLSetter("host")
+
 /**
  * @since 1.0.0
- * @category utils
+ * @category setters
  */
 export const setHostname: {
   (hostname: string): (url: URL) => URL
@@ -61,63 +62,70 @@ export const setHostname: {
 } = immutableURLSetter("hostname")
 /**
  * @since 1.0.0
- * @category utils
+ * @category setters
  */
 export const setHref: {
   (href: string): (url: URL) => URL
   (url: URL, href: string): URL
 } = immutableURLSetter("href")
+
 /**
  * @since 1.0.0
- * @category utils
+ * @category setters
  */
 export const setPassword: {
   (password: string): (url: URL) => URL
   (url: URL, password: string): URL
 } = immutableURLSetter("password")
+
 /**
  * @since 1.0.0
- * @category utils
+ * @category setters
  */
 export const setPathname: {
   (pathname: string): (url: URL) => URL
   (url: URL, pathname: string): URL
 } = immutableURLSetter("pathname")
+
 /**
  * @since 1.0.0
- * @category utils
+ * @category setters
  */
 export const setPort: {
   (port: string): (url: URL) => URL
   (url: URL, port: string): URL
 } = immutableURLSetter("port")
+
 /**
  * @since 1.0.0
- * @category utils
+ * @category setters
  */
 export const setProtocol: {
   (protocol: string): (url: URL) => URL
   (url: URL, protocol: string): URL
 } = immutableURLSetter("protocol")
+
 /**
  * @since 1.0.0
- * @category utils
+ * @category setters
  */
 export const setSearch: {
   (search: string): (url: URL) => URL
   (url: URL, search: string): URL
 } = immutableURLSetter("search")
+
 /**
  * @since 1.0.0
- * @category utils
+ * @category setters
  */
 export const setUsername: {
   (username: string): (url: URL) => URL
   (url: URL, username: string): URL
 } = immutableURLSetter("username")
+
 /**
  * @since 1.0.0
- * @category utils
+ * @category setters
  */
 export const setUrlParams: {
   (urlParams: UrlParams.UrlParams): (url: URL) => URL
@@ -127,6 +135,7 @@ export const setUrlParams: {
   result.search = UrlParams.toString(searchParams)
   return result
 })
+
 /**
  * @since 1.0.0
  * @category getters
@@ -134,6 +143,7 @@ export const setUrlParams: {
 export const urlParams: {
   (url: URL): UrlParams.UrlParams
 } = (url) => UrlParams.fromInput(url.searchParams)
+
 /**
  * @since 1.0.0
  * @category utils

From 23f135e9ead4b08406f801b7056323aa1cde4217 Mon Sep 17 00:00:00 2001
From: Maxim Khramtsov <khraks.mamtsov@gmail.com>
Date: Mon, 23 Dec 2024 10:01:59 +0100
Subject: [PATCH 4/9] cleanup

---
 packages/platform/test/Url.test.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/platform/test/Url.test.ts b/packages/platform/test/Url.test.ts
index c9abb7426f2..7b22c338846 100644
--- a/packages/platform/test/Url.test.ts
+++ b/packages/platform/test/Url.test.ts
@@ -13,6 +13,7 @@ describe("Url", () => {
         assert.notStrictEqual(url, testURL)
       }))
   })
+
   describe("setters", () => {
     it("immutable", () => {
       const hashUrl = Url.setHash(testURL, "test")
@@ -20,8 +21,9 @@ describe("Url", () => {
       assert.strictEqual(hashUrl.toString(), "https://example.com/test#test")
     })
   })
+
   it("modifyUrlParams", () => {
-    const paramsUrl = Url.modifyUrlParams(testURL, (x) => UrlParams.append(x, "key", "value"))
+    const paramsUrl = Url.modifyUrlParams(testURL, UrlParams.append("key", "value"))
     assert.notStrictEqual(paramsUrl, testURL)
     assert.strictEqual(paramsUrl.toString(), "https://example.com/test?key=value")
   })

From 08688c017a25d5de861d90d250114cbdb24589f4 Mon Sep 17 00:00:00 2001
From: Maxim Khramtsov <khraks.mamtsov@gmail.com>
Date: Wed, 8 Jan 2025 21:54:15 +0100
Subject: [PATCH 5/9] add copy

---
 packages/platform/src/Url.ts | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/packages/platform/src/Url.ts b/packages/platform/src/Url.ts
index c7c52ff02d6..0cfbcde8e4b 100644
--- a/packages/platform/src/Url.ts
+++ b/packages/platform/src/Url.ts
@@ -21,6 +21,14 @@ const immutableSetter = <M>(clone: (mutable: M) => M) =>
 /** @internal */
 const immutableURLSetter = immutableSetter<URL>((url) => new URL(url))
 
+/**
+ * @since 1.0.0
+ * @category constructors
+ */
+export const copy: {
+  (url: URL): URL
+} = (url) => new URL(url)
+
 /**
  * @since 1.0.0
  * @category constructors
@@ -131,7 +139,7 @@ export const setUrlParams: {
   (urlParams: UrlParams.UrlParams): (url: URL) => URL
   (url: URL, urlParams: UrlParams.UrlParams): URL
 } = dual(2, (url: URL, searchParams: UrlParams.UrlParams) => {
-  const result = new URL(url)
+  const result = copy(url)
   result.search = UrlParams.toString(searchParams)
   return result
 })
@@ -154,7 +162,7 @@ export const modifyUrlParams: {
 } = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams) => {
   const urlParams = UrlParams.fromInput(url.searchParams)
   const newUrlParams = f(urlParams)
-  const result = new URL(url)
+  const result = copy(url)
   result.search = UrlParams.toString(newUrlParams)
   return result
 })

From 4e2d863d3d5dd40f21d4bd6ac4d99b6253498873 Mon Sep 17 00:00:00 2001
From: Maxim Khramtsov <khraks.mamtsov@gmail.com>
Date: Wed, 8 Jan 2025 22:06:35 +0100
Subject: [PATCH 6/9] simplify make ctor

---
 packages/platform/src/Url.ts       |  4 ++--
 packages/platform/test/Url.test.ts | 14 +++++++++++---
 2 files changed, 13 insertions(+), 5 deletions(-)

diff --git a/packages/platform/src/Url.ts b/packages/platform/src/Url.ts
index 0cfbcde8e4b..010640b21ff 100644
--- a/packages/platform/src/Url.ts
+++ b/packages/platform/src/Url.ts
@@ -33,8 +33,8 @@ export const copy: {
  * @since 1.0.0
  * @category constructors
  */
-export const make: {
-  (url: string | URL, base?: string | URL | undefined): Either.Either<URL, Cause.IllegalArgumentException>
+export const fromString: {
+  (url: string, base?: string | URL | undefined): Either.Either<URL, Cause.IllegalArgumentException>
 } = (url, base) =>
   Either.try({
     try: () => new URL(url, base),
diff --git a/packages/platform/test/Url.test.ts b/packages/platform/test/Url.test.ts
index 7b22c338846..8b3cf30d9c9 100644
--- a/packages/platform/test/Url.test.ts
+++ b/packages/platform/test/Url.test.ts
@@ -1,19 +1,27 @@
 import * as Url from "@effect/platform/Url"
 import * as UrlParams from "@effect/platform/UrlParams"
 import { assert, describe, it } from "@effect/vitest"
-import { Effect } from "effect"
+import { Cause, Effect } from "effect"
 
 describe("Url", () => {
   const testURL = new URL("https://example.com/test")
 
-  describe("make", () => {
+  describe("copy", () => {
     it.effect("immutable", () =>
       Effect.gen(function*() {
-        const url = yield* Url.make(testURL)
+        const url = Url.copy(testURL)
         assert.notStrictEqual(url, testURL)
       }))
   })
 
+  describe("fromString", () => {
+    it.effect("fails on incorrect url", () =>
+      Effect.gen(function*() {
+        const error = yield* Url.fromString("??").pipe(Effect.flip)
+        assert.instanceOf(error, Cause.IllegalArgumentException)
+      }))
+  })
+
   describe("setters", () => {
     it("immutable", () => {
       const hashUrl = Url.setHash(testURL, "test")

From f1e7cc94cf6fbd6f77b3b0f16dfe884e9c39de09 Mon Sep 17 00:00:00 2001
From: Maxim Khramtsov <khraks.mamtsov@gmail.com>
Date: Thu, 9 Jan 2025 11:03:20 +0100
Subject: [PATCH 7/9] inline copy into immutableSetter for URL

---
 packages/platform/src/Url.ts | 42 ++++++++++++++++--------------------
 1 file changed, 19 insertions(+), 23 deletions(-)

diff --git a/packages/platform/src/Url.ts b/packages/platform/src/Url.ts
index 010640b21ff..5681fb2fc0a 100644
--- a/packages/platform/src/Url.ts
+++ b/packages/platform/src/Url.ts
@@ -6,29 +6,6 @@ import * as Either from "effect/Either"
 import { dual } from "effect/Function"
 import * as UrlParams from "./UrlParams.js"
 
-/** @internal */
-const immutableSetter = <M>(clone: (mutable: M) => M) =>
-<P extends keyof M>(property: P): {
-  (value: M[P]): (mutable: M) => M
-  (mutable: M, value: M[P]): M
-} =>
-  dual(2, (mutable: M, value: M[P]) => {
-    const result = clone(mutable)
-    result[property] = value
-    return result
-  })
-
-/** @internal */
-const immutableURLSetter = immutableSetter<URL>((url) => new URL(url))
-
-/**
- * @since 1.0.0
- * @category constructors
- */
-export const copy: {
-  (url: URL): URL
-} = (url) => new URL(url)
-
 /**
  * @since 1.0.0
  * @category constructors
@@ -42,6 +19,25 @@ export const fromString: {
       new Cause.IllegalArgumentException(cause instanceof globalThis.Error ? cause.message : "Invalid input")
   })
 
+/**
+ * @since 1.0.0
+ * @category constructors
+ */
+export const copy: {
+  (url: URL): URL
+} = (url) => new URL(url)
+
+/** @internal */
+const immutableURLSetter = <P extends keyof URL>(property: P): {
+  (value: URL[P]): (url: URL) => URL
+  (url: URL, value: URL[P]): URL
+} =>
+  dual(2, (url: URL, value: URL[P]) => {
+    const result = copy(url)
+    result[property] = value
+    return result
+  })
+
 /**
  * @since 1.0.0
  * @category setters

From 52ae514042f88da524fff3e1cca84eb24f05c3ec Mon Sep 17 00:00:00 2001
From: Tim <hello@timsmart.co>
Date: Tue, 14 Jan 2025 10:31:51 +1300
Subject: [PATCH 8/9] add Url.mutate

---
 packages/platform/src/Url.ts       | 43 +++++++++++++++---------------
 packages/platform/test/Url.test.ts |  5 ++--
 2 files changed, 25 insertions(+), 23 deletions(-)

diff --git a/packages/platform/src/Url.ts b/packages/platform/src/Url.ts
index 5681fb2fc0a..0eef012a8fb 100644
--- a/packages/platform/src/Url.ts
+++ b/packages/platform/src/Url.ts
@@ -21,22 +21,26 @@ export const fromString: {
 
 /**
  * @since 1.0.0
- * @category constructors
+ * @category utils
  */
-export const copy: {
-  (url: URL): URL
-} = (url) => new URL(url)
+export const mutate: {
+  (f: (url: URL) => void): (self: URL) => URL
+  (self: URL, f: (url: URL) => void): URL
+} = dual(2, (self: URL, f: (url: URL) => void) => {
+  const copy = new URL(self)
+  f(copy)
+  return copy
+})
 
 /** @internal */
 const immutableURLSetter = <P extends keyof URL>(property: P): {
   (value: URL[P]): (url: URL) => URL
   (url: URL, value: URL[P]): URL
 } =>
-  dual(2, (url: URL, value: URL[P]) => {
-    const result = copy(url)
-    result[property] = value
-    return result
-  })
+  dual(2, (url: URL, value: URL[P]) =>
+    mutate(url, (url) => {
+      url[property] = value
+    }))
 
 /**
  * @since 1.0.0
@@ -134,11 +138,10 @@ export const setUsername: {
 export const setUrlParams: {
   (urlParams: UrlParams.UrlParams): (url: URL) => URL
   (url: URL, urlParams: UrlParams.UrlParams): URL
-} = dual(2, (url: URL, searchParams: UrlParams.UrlParams) => {
-  const result = copy(url)
-  result.search = UrlParams.toString(searchParams)
-  return result
-})
+} = dual(2, (url: URL, searchParams: UrlParams.UrlParams) =>
+  mutate(url, (url) => {
+    url.search = UrlParams.toString(searchParams)
+  }))
 
 /**
  * @since 1.0.0
@@ -155,10 +158,8 @@ export const urlParams: {
 export const modifyUrlParams: {
   (f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): (url: URL) => URL
   (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): URL
-} = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams) => {
-  const urlParams = UrlParams.fromInput(url.searchParams)
-  const newUrlParams = f(urlParams)
-  const result = copy(url)
-  result.search = UrlParams.toString(newUrlParams)
-  return result
-})
+} = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams) =>
+  mutate(url, (url) => {
+    const params = f(UrlParams.fromInput(url.searchParams))
+    url.search = UrlParams.toString(params)
+  }))
diff --git a/packages/platform/test/Url.test.ts b/packages/platform/test/Url.test.ts
index 8b3cf30d9c9..aed4bfeee61 100644
--- a/packages/platform/test/Url.test.ts
+++ b/packages/platform/test/Url.test.ts
@@ -2,14 +2,15 @@ import * as Url from "@effect/platform/Url"
 import * as UrlParams from "@effect/platform/UrlParams"
 import { assert, describe, it } from "@effect/vitest"
 import { Cause, Effect } from "effect"
+import { constVoid } from "effect/Function"
 
 describe("Url", () => {
   const testURL = new URL("https://example.com/test")
 
-  describe("copy", () => {
+  describe("mutate", () => {
     it.effect("immutable", () =>
       Effect.gen(function*() {
-        const url = Url.copy(testURL)
+        const url = Url.mutate(testURL, constVoid)
         assert.notStrictEqual(url, testURL)
       }))
   })

From a51736498308d8c3fd4c26284aaa1cb6fcc46168 Mon Sep 17 00:00:00 2001
From: Tim <hello@timsmart.co>
Date: Tue, 14 Jan 2025 10:35:08 +1300
Subject: [PATCH 9/9] wip

---
 packages/platform/src/Url.ts | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/packages/platform/src/Url.ts b/packages/platform/src/Url.ts
index 0eef012a8fb..c63c6cf2b61 100644
--- a/packages/platform/src/Url.ts
+++ b/packages/platform/src/Url.ts
@@ -147,9 +147,7 @@ export const setUrlParams: {
  * @since 1.0.0
  * @category getters
  */
-export const urlParams: {
-  (url: URL): UrlParams.UrlParams
-} = (url) => UrlParams.fromInput(url.searchParams)
+export const urlParams = (url: URL): UrlParams.UrlParams => UrlParams.fromInput(url.searchParams)
 
 /**
  * @since 1.0.0