From 9ac021a3c6c8a53959b1ff5273bfac3993179fed Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 24 Jan 2023 06:12:12 +0000 Subject: [PATCH 1/5] Beginnings of `Window` wrapper --- dom/src/main/scala/fs2/dom/Clipboard.scala | 11 ++-- dom/src/main/scala/fs2/dom/Location.scala | 5 +- dom/src/main/scala/fs2/dom/LockManager.scala | 8 +-- dom/src/main/scala/fs2/dom/Navigator.scala | 45 ++++++++++++++++ dom/src/main/scala/fs2/dom/Storage.scala | 4 -- dom/src/main/scala/fs2/dom/Window.scala | 52 +++++++++++++++++++ .../test/scala/fs2/dom/LockManagerSuite.scala | 18 ++++--- .../src/test/scala/fs2/dom/StorageSuite.scala | 4 +- 8 files changed, 117 insertions(+), 30 deletions(-) create mode 100644 dom/src/main/scala/fs2/dom/Navigator.scala create mode 100644 dom/src/main/scala/fs2/dom/Window.scala diff --git a/dom/src/main/scala/fs2/dom/Clipboard.scala b/dom/src/main/scala/fs2/dom/Clipboard.scala index 4497431..028f899 100644 --- a/dom/src/main/scala/fs2/dom/Clipboard.scala +++ b/dom/src/main/scala/fs2/dom/Clipboard.scala @@ -17,7 +17,7 @@ package fs2.dom import cats.effect.kernel.Async -import org.scalajs.dom.window +import org.scalajs.dom abstract class Clipboard[F[_]] private { @@ -29,12 +29,13 @@ abstract class Clipboard[F[_]] private { object Clipboard { - def apply[F[_]](implicit F: Async[F]): Clipboard[F] = new Clipboard[F] { + private[dom] def apply[F[_]](clipboard: dom.Clipboard)(implicit F: Async[F]): Clipboard[F] = + new Clipboard[F] { - def readText = F.fromPromise(F.delay(window.navigator.clipboard.readText())) + def readText = F.fromPromise(F.delay(clipboard.readText())) - def writeText(text: String) = F.fromPromise(F.delay(window.navigator.clipboard.writeText(text))) + def writeText(text: String) = F.fromPromise(F.delay(clipboard.writeText(text))) - } + } } diff --git a/dom/src/main/scala/fs2/dom/Location.scala b/dom/src/main/scala/fs2/dom/Location.scala index c144201..3e044ef 100644 --- a/dom/src/main/scala/fs2/dom/Location.scala +++ b/dom/src/main/scala/fs2/dom/Location.scala @@ -50,10 +50,7 @@ abstract class Location[F[_]] private { object Location { - def apply[F[_]: Sync]: Location[F] = - apply(dom.window.location) - - private def apply[F[_]](location: dom.Location)(implicit F: Sync[F]): Location[F] = + private[dom] def apply[F[_]](location: dom.Location)(implicit F: Sync[F]): Location[F] = new Location[F] { def href = new WrappedRef[F, String] { diff --git a/dom/src/main/scala/fs2/dom/LockManager.scala b/dom/src/main/scala/fs2/dom/LockManager.scala index d8ab834..1f0a9df 100644 --- a/dom/src/main/scala/fs2/dom/LockManager.scala +++ b/dom/src/main/scala/fs2/dom/LockManager.scala @@ -24,9 +24,6 @@ import cats.effect.syntax.all._ import cats.syntax.all._ import fs2.dom.facade.LockRequestOptions import org.scalajs.dom -import org.scalajs.dom.window - -import scala.scalajs.js abstract class LockManager[F[_]] private { @@ -42,10 +39,7 @@ abstract class LockManager[F[_]] private { object LockManager { - def apply[F[_]: Async]: LockManager[F] = - apply(window.navigator.asInstanceOf[js.Dynamic].locks.asInstanceOf[facade.LockManager]) - - private def apply[F[_]](manager: facade.LockManager)(implicit F: Async[F]): LockManager[F] = + private[dom] def apply[F[_]](manager: facade.LockManager)(implicit F: Async[F]): LockManager[F] = new LockManager[F] { def exclusive(name: String) = request(name, "exclusive", false).void diff --git a/dom/src/main/scala/fs2/dom/Navigator.scala b/dom/src/main/scala/fs2/dom/Navigator.scala new file mode 100644 index 0000000..3cf4157 --- /dev/null +++ b/dom/src/main/scala/fs2/dom/Navigator.scala @@ -0,0 +1,45 @@ +/* + * Copyright 2022 Arman Bilge + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fs2.dom + +import cats.effect.kernel.Async +import org.scalajs.dom + +import scala.scalajs.js + +abstract class Navigator[F[_]] private { + + def clipboard: Clipboard[F] + + def locks: LockManager[F] + +} + +object Navigator { + + private[dom] def apply[F[_]](navigator: dom.Navigator)(implicit F: Async[F]): Navigator[F] = + new Navigator[F] { + + def clipboard = Clipboard(navigator.clipboard) + + def locks = LockManager( + navigator.asInstanceOf[js.Dynamic].locks.asInstanceOf[facade.LockManager] + ) + + } + +} diff --git a/dom/src/main/scala/fs2/dom/Storage.scala b/dom/src/main/scala/fs2/dom/Storage.scala index 925350f..91d14f4 100644 --- a/dom/src/main/scala/fs2/dom/Storage.scala +++ b/dom/src/main/scala/fs2/dom/Storage.scala @@ -41,10 +41,6 @@ abstract class Storage[F[_]] private { object Storage { - def local[F[_]: Async]: Storage[F] = apply(dom.window.localStorage) - - def session[F[_]: Async]: Storage[F] = apply(dom.window.sessionStorage) - sealed abstract class Event { def url: String } diff --git a/dom/src/main/scala/fs2/dom/Window.scala b/dom/src/main/scala/fs2/dom/Window.scala new file mode 100644 index 0000000..bdcbe02 --- /dev/null +++ b/dom/src/main/scala/fs2/dom/Window.scala @@ -0,0 +1,52 @@ +/* + * Copyright 2022 Arman Bilge + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fs2.dom + +import cats.effect.kernel.Async +import org.scalajs.dom + +abstract class Window[F[_]] private { + + def localStorage: Storage[F] + + def location: Location[F] + + def navigator: Navigator[F] + + def sessionStorage: Storage[F] + +} + +object Window { + + def apply[F[_]](implicit F: Async[F]): Window[F] = + apply(dom.window) + + def apply[F[_]](window: dom.Window)(implicit F: Async[F]): Window[F] = + new Window[F] { + + def localStorage = Storage(window.localStorage) + + def location = Location(window.location) + + def navigator = Navigator(window.navigator) + + def sessionStorage = Storage(window.sessionStorage) + + } + +} diff --git a/testsBrowser/src/test/scala/fs2/dom/LockManagerSuite.scala b/testsBrowser/src/test/scala/fs2/dom/LockManagerSuite.scala index 753be94..e805070 100644 --- a/testsBrowser/src/test/scala/fs2/dom/LockManagerSuite.scala +++ b/testsBrowser/src/test/scala/fs2/dom/LockManagerSuite.scala @@ -23,34 +23,36 @@ import scala.concurrent.duration._ class LockManagerSuite extends CatsEffectSuite { + def locks = Window[IO].navigator.locks + test("no contention") { IO.ref(false).flatMap { ref => - LockManager[IO].exclusive("lock").surround(ref.set(true)) *> ref.get.assert + locks.exclusive("lock").surround(ref.set(true)) *> ref.get.assert } } test("use cancelable") { - val cancel = LockManager[IO].exclusive("lock").surround(IO.never).timeoutTo(1.second, IO.unit) + val cancel = locks.exclusive("lock").surround(IO.never).timeoutTo(1.second, IO.unit) val reacquire = IO.ref(false).flatMap { ref => - LockManager[IO].exclusive("lock").surround(ref.set(true)) *> ref.get.assert + locks.exclusive("lock").surround(ref.set(true)) *> ref.get.assert } cancel *> reacquire } test("exclusivity") { - LockManager[IO].exclusive("lock").surround(IO.never).start.flatMap { f => + locks.exclusive("lock").surround(IO.never).start.flatMap { f => IO.sleep(1.second) *> - LockManager[IO].tryExclusive("lock").use(IO.pure(_)).map(!_).assert *> + locks.tryExclusive("lock").use(IO.pure(_)).map(!_).assert *> f.cancel *> - LockManager[IO].tryExclusive("lock").use(IO.pure(_)).assert + locks.tryExclusive("lock").use(IO.pure(_)).assert } } test("acquire cancelability") { - LockManager[IO].exclusive("lock").surround { + locks.exclusive("lock").surround { IO.ref(true).flatMap { ref => - LockManager[IO] + locks .exclusive("lock") .surround(ref.set(false)) .as(false) diff --git a/testsBrowser/src/test/scala/fs2/dom/StorageSuite.scala b/testsBrowser/src/test/scala/fs2/dom/StorageSuite.scala index 6968d8a..0454c60 100644 --- a/testsBrowser/src/test/scala/fs2/dom/StorageSuite.scala +++ b/testsBrowser/src/test/scala/fs2/dom/StorageSuite.scala @@ -49,6 +49,6 @@ abstract class StorageSuite(storage: Storage[IO]) extends CatsEffectSuite { } -class LocalStorageSuite extends StorageSuite(Storage.local) +class LocalStorageSuite extends StorageSuite(Window[IO].localStorage) -class SessionStorageSuite extends StorageSuite(Storage.session) +class SessionStorageSuite extends StorageSuite(Window[IO].sessionStorage) From 4ea1d086ab2bfc80ea0e582042f766a8faa8c6b9 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 31 Jan 2023 03:21:46 +0000 Subject: [PATCH 2/5] Update `Storage` for new events wrapper --- dom/src/main/scala/fs2/dom/Storage.scala | 48 ++++++++++++------------ dom/src/main/scala/fs2/dom/Window.scala | 6 +++ 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/dom/src/main/scala/fs2/dom/Storage.scala b/dom/src/main/scala/fs2/dom/Storage.scala index 91d14f4..9640378 100644 --- a/dom/src/main/scala/fs2/dom/Storage.scala +++ b/dom/src/main/scala/fs2/dom/Storage.scala @@ -18,12 +18,12 @@ package fs2 package dom import cats.syntax.all._ -import cats.effect.kernel.Async +import cats.effect.kernel.Sync import org.scalajs.dom abstract class Storage[F[_]] private { - def events: Stream[F, Storage.Event] + def events(window: Window[F]): Stream[F, Storage.Event] def length: F[Int] @@ -52,9 +52,9 @@ object Storage { final case class Updated private (key: String, oldValue: String, newValue: String, url: String) extends Event - private[Storage] def fromStorageEvent(ev: dom.StorageEvent): Event = - Option(ev.key).fold[Event](Cleared(ev.url)) { key => - (Option(ev.oldValue), Option(ev.newValue)) match { + private[Storage] def fromStorageEvent[F[_]](ev: StorageEvent[F]): Event = + ev.key.fold[Event](Cleared(ev.url)) { key => + (ev.oldValue, ev.newValue) match { case (Some(oldValue), None) => Removed(key, oldValue, ev.url) case (None, Some(newValue)) => Added(key, newValue, ev.url) case (Some(oldValue), Some(newValue)) => Updated(key, oldValue, newValue, ev.url) @@ -63,31 +63,31 @@ object Storage { } } - private[dom] def apply[F[_]](storage: dom.Storage)(implicit F: Async[F]): Storage[F] = - new Storage[F] { + private[dom] def apply[F[_]: Sync](storage: dom.Storage): Storage[F] = + new WrappedStorage(storage) - def events = - fs2.dom.events[F, dom.StorageEvent](dom.window, "storage").mapFilter { ev => - if (ev.storageArea eq storage) - Some(Event.fromStorageEvent(ev)) - else - None - } + private final case class WrappedStorage[F[_]](storage: dom.Storage)(implicit F: Sync[F]) + extends Storage[F] { + + def events(window: Window[F]) = + window.storageEvents.mapFilter { ev => + Option.when(ev.storageArea == this)(Event.fromStorageEvent(ev)) + } - def length = F.delay(storage.length) + def length = F.delay(storage.length) - def getItem(key: String) = - F.delay(Option(storage.getItem(key))) + def getItem(key: String) = + F.delay(Option(storage.getItem(key))) - def setItem(key: String, item: String) = - F.delay(storage.setItem(key, item)) + def setItem(key: String, item: String) = + F.delay(storage.setItem(key, item)) - def removeItem(key: String) = - F.delay(storage.removeItem(key)) + def removeItem(key: String) = + F.delay(storage.removeItem(key)) - def key(i: Int) = F.delay(Option(storage.key(i))) + def key(i: Int) = F.delay(Option(storage.key(i))) - def clear = F.delay(storage.clear()) + def clear = F.delay(storage.clear()) - } + } } diff --git a/dom/src/main/scala/fs2/dom/Window.scala b/dom/src/main/scala/fs2/dom/Window.scala index bdcbe02..2934b7f 100644 --- a/dom/src/main/scala/fs2/dom/Window.scala +++ b/dom/src/main/scala/fs2/dom/Window.scala @@ -17,6 +17,7 @@ package fs2.dom import cats.effect.kernel.Async +import fs2.Stream import org.scalajs.dom abstract class Window[F[_]] private { @@ -29,6 +30,8 @@ abstract class Window[F[_]] private { def sessionStorage: Storage[F] + def storageEvents: Stream[F, StorageEvent[F]] + } object Window { @@ -47,6 +50,9 @@ object Window { def sessionStorage = Storage(window.sessionStorage) + def storageEvents: Stream[F, StorageEvent[F]] = + events[F, dom.StorageEvent](window, "storage").map(StorageEvent(_)) + } } From 59380f8f30556a42d332a47d56dcd4f1aff270b2 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 31 Jan 2023 03:27:02 +0000 Subject: [PATCH 3/5] Expose `History` through `Window` --- dom/src/main/scala/fs2/dom/History.scala | 30 +++++++++++++----------- dom/src/main/scala/fs2/dom/Window.scala | 4 ++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/dom/src/main/scala/fs2/dom/History.scala b/dom/src/main/scala/fs2/dom/History.scala index 3e69848..0145624 100644 --- a/dom/src/main/scala/fs2/dom/History.scala +++ b/dom/src/main/scala/fs2/dom/History.scala @@ -26,7 +26,6 @@ import fs2.concurrent.Signal import org.scalajs.dom import org.scalajs.dom.EventListenerOptions import org.scalajs.dom.ScrollRestoration -import org.scalajs.dom.window abstract class History[F[_], S] private { @@ -48,7 +47,10 @@ abstract class History[F[_], S] private { } object History { - def apply[F[_], S](implicit F: Async[F], serializer: Serializer[F, S]): History[F, S] = + private[dom] def apply[F[_], S](window: dom.Window, history: dom.History)(implicit + F: Async[F], + serializer: Serializer[F, S] + ): History[F, S] = new History[F, S] { def state = new Signal[F, Option[S]] { @@ -62,7 +64,7 @@ object History { (head ++ tail).concurrently(listener) } - def get = OptionT(F.delay(Option(window.history.state))) + def get = OptionT(F.delay(Option(history.state))) .semiflatMap(serializer.deserialize(_)) .value @@ -71,29 +73,29 @@ object History { def length = new Signal[F, Int] { def discrete = state.discrete.evalMap(_ => get) - def get = F.delay(window.history.length) + def get = F.delay(history.length) def continuous = Stream.repeatEval(get) } def scrollRestoration = new WrappedRef( - () => window.history.scrollRestoration, - window.history.scrollRestoration = _ + () => history.scrollRestoration, + history.scrollRestoration = _ ) - def forward = asyncPopState(window.history.forward()) - def back = asyncPopState(window.history.back()) - def go = asyncPopState(window.history.go()) - def go(delta: Int) = asyncPopState(window.history.go(delta)) + def forward = asyncPopState(history.forward()) + def back = asyncPopState(history.back()) + def go = asyncPopState(history.go()) + def go(delta: Int) = asyncPopState(history.go(delta)) def pushState(state: S) = - serializer.serialize(state).flatMap(s => F.delay(window.history.pushState(s, ""))) + serializer.serialize(state).flatMap(s => F.delay(history.pushState(s, ""))) def pushState(state: S, url: String) = - serializer.serialize(state).flatMap(s => F.delay(window.history.pushState(s, "", url))) + serializer.serialize(state).flatMap(s => F.delay(history.pushState(s, "", url))) def replaceState(state: S) = - serializer.serialize(state).flatMap(s => F.delay(window.history.replaceState(s, ""))) + serializer.serialize(state).flatMap(s => F.delay(history.replaceState(s, ""))) def replaceState(state: S, url: String) = - serializer.serialize(state).flatMap(s => F.delay(window.history.replaceState(s, "", url))) + serializer.serialize(state).flatMap(s => F.delay(history.replaceState(s, "", url))) def asyncPopState(thunk: => Unit): F[Unit] = F.async_[Unit] { cb => window.addEventListener[dom.PopStateEvent]( diff --git a/dom/src/main/scala/fs2/dom/Window.scala b/dom/src/main/scala/fs2/dom/Window.scala index 2934b7f..bb0a406 100644 --- a/dom/src/main/scala/fs2/dom/Window.scala +++ b/dom/src/main/scala/fs2/dom/Window.scala @@ -22,6 +22,8 @@ import org.scalajs.dom abstract class Window[F[_]] private { + def history[S](implicit serializer: Serializer[F, S]): History[F, S] + def localStorage: Storage[F] def location: Location[F] @@ -42,6 +44,8 @@ object Window { def apply[F[_]](window: dom.Window)(implicit F: Async[F]): Window[F] = new Window[F] { + def history[S](implicit serializer: Serializer[F, S]) = History(window, window.history) + def localStorage = Storage(window.localStorage) def location = Location(window.location) From c4e74119c07e1d1d49a4faa36053347884e3039b Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 31 Jan 2023 03:33:58 +0000 Subject: [PATCH 4/5] Fix `HistorySuite` --- testsBrowser/src/test/scala/fs2/dom/HistorySuite.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testsBrowser/src/test/scala/fs2/dom/HistorySuite.scala b/testsBrowser/src/test/scala/fs2/dom/HistorySuite.scala index b49c85f..e80f2ca 100644 --- a/testsBrowser/src/test/scala/fs2/dom/HistorySuite.scala +++ b/testsBrowser/src/test/scala/fs2/dom/HistorySuite.scala @@ -25,7 +25,7 @@ import scala.concurrent.duration._ class HistorySuite extends CatsEffectSuite { - val history = History[IO, Int] + val history = Window[IO].history[Int] test("history") { Channel.unbounded[IO, Int].flatMap { ch => From f5c78b3df535bcdca946108cd198aed26e4e9563 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 31 Jan 2023 04:02:52 +0000 Subject: [PATCH 5/5] Expose `Window#document` --- .../scala-2/fs2/dom/WindowCrossCompat.scala | 21 ++++++++++ dom/src/main/scala-3/fs2/dom/Dom.scala | 2 + .../scala-3/fs2/dom/WindowCrossCompat.scala | 38 +++++++++++++++++++ dom/src/main/scala/fs2/dom/Window.scala | 8 ++-- 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 dom/src/main/scala-2/fs2/dom/WindowCrossCompat.scala create mode 100644 dom/src/main/scala-3/fs2/dom/WindowCrossCompat.scala diff --git a/dom/src/main/scala-2/fs2/dom/WindowCrossCompat.scala b/dom/src/main/scala-2/fs2/dom/WindowCrossCompat.scala new file mode 100644 index 0000000..9dcdfcd --- /dev/null +++ b/dom/src/main/scala-2/fs2/dom/WindowCrossCompat.scala @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Arman Bilge + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fs2.dom + +private[dom] trait WindowCrossCompat[F[_]] + +private trait WindowImplCrossCompat[F[_]] extends WindowCrossCompat[F] diff --git a/dom/src/main/scala-3/fs2/dom/Dom.scala b/dom/src/main/scala-3/fs2/dom/Dom.scala index 0173ca0..b7bdacc 100644 --- a/dom/src/main/scala-3/fs2/dom/Dom.scala +++ b/dom/src/main/scala-3/fs2/dom/Dom.scala @@ -66,6 +66,8 @@ object Document { } } +opaque type HtmlDocument[F[_]] <: Document[F] = dom.HTMLDocument + opaque type Element[F[_]] <: Node[F] = dom.Element opaque type HtmlElement[F[_]] <: Element[F] = dom.HTMLElement opaque type HtmlAnchorElement[F[_]] <: HtmlElement[F] = dom.HTMLAnchorElement diff --git a/dom/src/main/scala-3/fs2/dom/WindowCrossCompat.scala b/dom/src/main/scala-3/fs2/dom/WindowCrossCompat.scala new file mode 100644 index 0000000..1c0a748 --- /dev/null +++ b/dom/src/main/scala-3/fs2/dom/WindowCrossCompat.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Arman Bilge + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fs2.dom + +import cats.effect.kernel.Async +import org.scalajs.dom + +private trait WindowCrossCompat[F[_]] { + + implicit def given_Dom_F: Dom[F] + + def document: HtmlDocument[F] + +} + +private trait WindowImplCrossCompat[F[_]](using Async[F]) extends WindowCrossCompat[F] { + + private[dom] def window: dom.Window + + implicit def given_Dom_F = Dom.forAsync + + def document = window.document.asInstanceOf[HtmlDocument[F]] + +} diff --git a/dom/src/main/scala/fs2/dom/Window.scala b/dom/src/main/scala/fs2/dom/Window.scala index bb0a406..ebd172c 100644 --- a/dom/src/main/scala/fs2/dom/Window.scala +++ b/dom/src/main/scala/fs2/dom/Window.scala @@ -20,7 +20,7 @@ import cats.effect.kernel.Async import fs2.Stream import org.scalajs.dom -abstract class Window[F[_]] private { +abstract class Window[F[_]] private extends WindowCrossCompat[F] { def history[S](implicit serializer: Serializer[F, S]): History[F, S] @@ -41,8 +41,10 @@ object Window { def apply[F[_]](implicit F: Async[F]): Window[F] = apply(dom.window) - def apply[F[_]](window: dom.Window)(implicit F: Async[F]): Window[F] = - new Window[F] { + private def apply[F[_]](_window: dom.Window)(implicit F: Async[F]): Window[F] = + new Window[F] with WindowImplCrossCompat[F] { + + private[dom] def window = _window def history[S](implicit serializer: Serializer[F, S]) = History(window, window.history)