Skip to content

Commit 3a407eb

Browse files
authored
Merge pull request #11 from lolgab/fix-timing-issue
Fix timing issues waiting for scala-cli linking
2 parents 1a63ef3 + 7ad82b8 commit 3a407eb

File tree

5 files changed

+71
-79
lines changed

5 files changed

+71
-79
lines changed

project/src/build.runner.scala

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,33 @@ import cats.effect.IO
1616
import cats.effect.OutcomeIO
1717
import cats.effect.ResourceIO
1818

19+
import scala.concurrent.duration.*
20+
1921
sealed trait BuildTool
2022
class ScalaCli extends BuildTool
2123
class Mill extends BuildTool
2224

2325
def buildRunner(
2426
tool: BuildTool,
25-
refreshTopic: Topic[IO, String],
27+
linkingTopic: Topic[IO, Unit],
2628
workDir: fs2.io.file.Path,
2729
outDir: fs2.io.file.Path,
2830
extraBuildArgs: List[String],
2931
millModuleName: Option[String]
3032
)(
3133
logger: Scribe[IO]
3234
): ResourceIO[IO[OutcomeIO[Unit]]] = tool match
33-
case scli: ScalaCli => buildRunnerScli(refreshTopic, workDir, outDir, extraBuildArgs)(logger)
35+
case scli: ScalaCli => buildRunnerScli(linkingTopic, workDir, outDir, extraBuildArgs)(logger)
3436
case m: Mill =>
3537
buildRunnerMill(
36-
refreshTopic,
38+
linkingTopic,
3739
workDir,
3840
millModuleName.getOrElse(throw new Exception("must have a module name when running with mill")),
3941
extraBuildArgs
4042
)(logger)
4143

4244
def buildRunnerScli(
43-
refreshTopic: Topic[IO, String],
45+
linkingTopic: Topic[IO, Unit],
4446
workDir: fs2.io.file.Path,
4547
outDir: fs2.io.file.Path,
4648
extraBuildArgs: List[String]
@@ -79,7 +81,7 @@ def buildRunnerScli(
7981
aChunk =>
8082
if aChunk.toString.contains("main.js, run it with") then
8183
logger.trace("Detected that linking was successful, emitting refresh event") >>
82-
refreshTopic.publish1("refresh")
84+
linkingTopic.publish1(())
8385
else
8486
logger.trace(s"$aChunk :: Linking unfinished") >>
8587
IO.unit
@@ -92,7 +94,7 @@ def buildRunnerScli(
9294
end buildRunnerScli
9395

9496
def buildRunnerMill(
95-
refreshTopic: Topic[IO, String],
97+
linkingTopic: Topic[IO, Unit],
9698
workDir: fs2.io.file.Path,
9799
moduleName: String,
98100
extraBuildArgs: List[String]
@@ -105,15 +107,15 @@ def buildRunnerMill(
105107
.Stream
106108
.resource(Watcher.default[IO].evalTap(_.watch(watchLinkComplePath.toNioPath)))
107109
.flatMap {
108-
_.events()
110+
_.events(100.millis)
109111
.evalTap {
110112
(e: Event) =>
111113
e match
112114
case Created(path, count) => logger.info("fastLinkJs.json was created")
113115
case Deleted(path, count) => logger.info("fastLinkJs.json was deleted")
114116
case Modified(path, count) =>
115117
logger.info("fastLinkJs.json was modified - link successful => trigger a refresh") >>
116-
refreshTopic.publish1("refresh")
118+
linkingTopic.publish1(())
117119
case Overflow(count) => logger.info("overflow")
118120
case NonStandard(event, registeredDirectory) => logger.info("non-standard")
119121

project/src/hasher.scala

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
import java.security.MessageDigest
2-
31
import cats.effect.IO
2+
import fs2.io.file.*
43

5-
val md = MessageDigest.getInstance("MD5")
4+
// TODO: Use last modified time once scala-cli stops
5+
// copy pasting the files from a temporary directory
6+
// and performs linking in place
7+
// def fileHash(filePath: Path): IO[String] =
8+
// Files[IO].getLastModifiedTime(filePath).map(_.toNanos.toString)
69

7-
def fielHash(filePath: fs2.io.file.Path): IO[String] =
8-
fs2
9-
.io
10-
.file
11-
.Files[IO]
12-
.readUtf8Lines(filePath)
13-
.compile
14-
.toList
15-
.map(lines => md.digest(lines.mkString("\n").getBytes).map("%02x".format(_)).mkString)
10+
def fileHash(filePath: fs2.io.file.Path): IO[String] =
11+
Files[IO].readAll(filePath).through(fs2.hash.md5).through(fs2.text.hex.encode).compile.string

project/src/live.server.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,14 +268,15 @@ object LiveServer
268268

269269
fileToHashRef <- Ref[IO].of(Map.empty[String, String]).toResource
270270
fileToHashMapRef = MapRef.fromSingleImmutableMapRef[IO, String, String](fileToHashRef)
271-
refreshPub <- Topic[IO, String].toResource
271+
refreshTopic <- Topic[IO, Unit].toResource
272+
linkingTopic <- Topic[IO, Unit].toResource
272273
client <- EmberClientBuilder.default[IO].build
273274

274275
proxyRoutes: HttpRoutes[IO] <- makeProxyRoutes(client, pathPrefix, proxyConfig)
275276

276277
_ <- buildRunner(
277278
buildTool,
278-
refreshPub,
279+
linkingTopic,
279280
fs2.io.file.Path(baseDir),
280281
fs2.io.file.Path(outDir),
281282
extraBuildArgs,
@@ -284,11 +285,13 @@ object LiveServer
284285

285286
indexHtmlTemplate = externalIndexHtmlTemplate.getOrElse(vanillaTemplate(stylesDir.isDefined).render)
286287

287-
app <- routes(outDir.toString(), refreshPub, stylesDir, proxyRoutes, indexHtmlTemplate, fileToHashRef)(logger)
288+
app <- routes(outDir.toString(), refreshTopic, stylesDir, proxyRoutes, indexHtmlTemplate, fileToHashRef)(
289+
logger
290+
)
288291

289292
_ <- seedMapOnStart(outDir, fileToHashMapRef)(logger)
290293
// _ <- stylesDir.fold(Resource.unit)(sd => seedMapOnStart(sd, mr))
291-
_ <- fileWatcher(fs2.io.file.Path(outDir), fileToHashMapRef)(logger)
294+
_ <- fileWatcher(fs2.io.file.Path(outDir), fileToHashMapRef, linkingTopic, refreshTopic)(logger)
292295
// _ <- stylesDir.fold(Resource.unit[IO])(sd => fileWatcher(fs2.io.file.Path(sd), mr))
293296
_ <- logger.info(s"Start dev server on http://localhost:$port").toResource
294297
server <- buildServer(app, port)

project/src/routes.scala

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import org.typelevel.ci.CIStringSyntax
1616

1717
import fs2.*
1818
import fs2.concurrent.Topic
19-
import fs2.io.Watcher
20-
import fs2.io.Watcher.Event
2119
import fs2.io.file.Files
2220

2321
import scribe.Scribe
@@ -29,7 +27,6 @@ import cats.effect.IO
2927
import cats.effect.kernel.Ref
3028
import cats.effect.kernel.Resource
3129
import cats.effect.std.*
32-
import cats.effect.std.MapRef
3330
import cats.syntax.all.*
3431

3532
import _root_.io.circe.syntax.EncoderOps
@@ -89,7 +86,7 @@ end ETagMiddleware
8986

9087
def routes(
9188
stringPath: String,
92-
refreshTopic: Topic[IO, String],
89+
refreshTopic: Topic[IO, Unit],
9390
stylesPath: Option[String],
9491
proxyRoutes: HttpRoutes[IO],
9592
indexHtmlTemplate: String,
@@ -162,7 +159,7 @@ def seedMapOnStart(stringPath: String, mr: MapRef[IO, String, Option[String]])(l
162159
.isRegularFile(f)
163160
.ifM(
164161
// logger.trace(s"hashing $f") >>
165-
fielHash(f).flatMap(
162+
fileHash(f).flatMap(
166163
h =>
167164
val key = asFs2.relativize(f)
168165
logger.trace(s"hashing $f to put at $key with hash : $h") >>
@@ -179,46 +176,33 @@ end seedMapOnStart
179176

180177
private def fileWatcher(
181178
stringPath: fs2.io.file.Path,
182-
mr: MapRef[IO, String, Option[String]]
183-
)(logger: Scribe[IO]): ResourceIO[IO[OutcomeIO[Unit]]] =
184-
fs2
185-
.Stream
186-
.resource(Watcher.default[IO].evalTap(_.watch(stringPath.toNioPath)))
187-
.flatMap {
188-
w =>
189-
w.events()
190-
.evalTap(
191-
(e: Event) =>
192-
e match
193-
case Event.Created(path, i) =>
194-
// if path.endsWith(".js") then
195-
logger.trace(s"created $path, calculating hash") >>
196-
fielHash(fs2.io.file.Path(path.toString())).flatMap(
197-
h =>
198-
val serveAt = stringPath.relativize(fs2.io.file.Path(path.toString()))
199-
logger.trace(s"$serveAt :: hash -> $h") >>
200-
mr.setKeyValue(serveAt.toString(), h)
201-
)
202-
// else IO.unit
203-
case Event.Modified(path, i) =>
204-
// if path.endsWith(".js") then
205-
logger.trace(s"modified $path, calculating hash") >>
206-
fielHash(fs2.io.file.Path(path.toString())).flatMap(
207-
h =>
208-
val serveAt = stringPath.relativize(fs2.io.file.Path(path.toString()))
209-
logger.trace(s"$serveAt :: hash -> $h") >>
210-
mr.setKeyValue(serveAt.toString(), h)
211-
)
212-
// else IO.unit
213-
case Event.Deleted(path, i) =>
179+
mr: MapRef[IO, String, Option[String]],
180+
linkingTopic: Topic[IO, Unit],
181+
refreshTopic: Topic[IO, Unit]
182+
)(logger: Scribe[IO]): ResourceIO[Unit] =
183+
linkingTopic
184+
.subscribe(10)
185+
.evalTap {
186+
_ =>
187+
fs2
188+
.io
189+
.file
190+
.Files[IO]
191+
.list(stringPath)
192+
.evalTap {
193+
path =>
194+
fileHash(path).flatMap(
195+
h =>
214196
val serveAt = stringPath.relativize(fs2.io.file.Path(path.toString()))
215-
logger.trace(s"deleted $path, removing key $serveAt") >>
216-
mr.unsetKey(serveAt.toString())
217-
case e: Event.Overflow => logger.info("overflow")
218-
case e: Event.NonStandard => logger.info("non-standard")
219-
)
197+
logger.trace(s"$serveAt :: hash -> $h") >>
198+
mr.setKeyValue(serveAt.toString(), h)
199+
)
200+
}
201+
.compile
202+
.drain >> refreshTopic.publish1(())
220203
}
221204
.compile
222205
.drain
223206
.background
207+
.void
224208
end fileWatcher

project/test/src/RoutesSpec.scala

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import java.security.MessageDigest
22

3-
import scala.concurrent.duration.DurationInt
4-
53
import org.http4s.HttpRoutes
64
import org.typelevel.ci.CIStringSyntax
75

@@ -13,7 +11,8 @@ import cats.effect.kernel.Ref
1311
import cats.effect.std.MapRef
1412

1513
import munit.CatsEffectSuite
16-
// import cats.effect.unsafe.implicits.global
14+
15+
import scala.concurrent.duration.*
1716

1817
class ExampleSuite extends CatsEffectSuite:
1918

@@ -56,22 +55,30 @@ class ExampleSuite extends CatsEffectSuite:
5655

5756
files.test("watched map is updated") {
5857
tempDir =>
59-
val newStr = "const hi = 'bye, world'"
60-
val newHash = md.digest(testStr.getBytes()).map("%02x".format(_)).mkString
6158
val toCheck = for
6259
logger <- IO(scribe.cats[IO]).toResource
6360
fileToHashRef <- Ref[IO].of(Map.empty[String, String]).toResource
61+
linkingTopic <- Topic[IO, Unit].toResource
62+
refreshTopic <- Topic[IO, Unit].toResource
6463
fileToHashMapRef = MapRef.fromSingleImmutableMapRef[IO, String, String](fileToHashRef)
64+
_ <- fileWatcher(fs2.io.file.Path(tempDir.toString), fileToHashMapRef, linkingTopic, refreshTopic)(logger)
65+
_ <- IO.sleep(100.millis).toResource // wait for watcher to start
6566
_ <- seedMapOnStart(tempDir.toString, fileToHashMapRef)(logger)
66-
_ <- fileWatcher(fs2.io.file.Path(tempDir.toString), fileToHashMapRef)(logger)
67-
_ <- IO(os.write.over(tempDir / "test.js", newStr)).toResource
68-
_ <- IO.sleep(1.second).toResource
69-
updatedMap <- fileToHashRef.get.toResource
70-
yield updatedMap
67+
_ <- IO.blocking(os.write.over(tempDir / "test.js", "const hi = 'bye, world'")).toResource
68+
_ <- linkingTopic.publish1(()).toResource
69+
_ <- refreshTopic.subscribe(1).head.compile.resource.drain
70+
oldHash <- fileToHashRef.get.map(_("test.js")).toResource
71+
_ <- IO.blocking(os.write.over(tempDir / "test.js", "const hi = 'modified, workd'")).toResource
72+
_ <- linkingTopic.publish1(()).toResource
73+
_ <- refreshTopic.subscribe(1).head.compile.resource.drain
74+
newHash <- fileToHashRef.get.map(_("test.js")).toResource
75+
yield oldHash -> newHash
7176

7277
toCheck.use {
73-
updatedMap =>
74-
assertIO(IO(updatedMap.get("test.js")), Some(newHash))
78+
case (oldHash, newHash) =>
79+
IO(assertNotEquals(oldHash, newHash)) >>
80+
IO(assertEquals(oldHash, "27b2d040a66fb938f134c4b66fb7e9ce")) >>
81+
IO(assertEquals(newHash, "3ebb82d4d6236c6bfbb90d65943b3e3d"))
7582
}
7683

7784
}
@@ -84,7 +91,7 @@ class ExampleSuite extends CatsEffectSuite:
8491
fileToHashRef <- Ref[IO].of(Map.empty[String, String]).toResource
8592
fileToHashMapRef = MapRef.fromSingleImmutableMapRef[IO, String, String](fileToHashRef)
8693
_ <- seedMapOnStart(tempDir.toString, fileToHashMapRef)(logger)
87-
refreshPub <- Topic[IO, String].toResource
94+
refreshPub <- Topic[IO, Unit].toResource
8895
theseRoutes <- routes(
8996
tempDir.toString,
9097
refreshPub,

0 commit comments

Comments
 (0)