Persisters now use an injected KeyValueStore to facilitate testing of persistence code. Added LocalStorage and in-memory implementation of KeyValueStore.

This commit is contained in:
Daan Vanden Bosch 2021-11-30 22:13:46 +01:00
parent f629f56e3a
commit 6374a3f054
13 changed files with 73 additions and 16 deletions

View File

@ -18,6 +18,7 @@ import world.phantasmal.core.disposable.disposable
import world.phantasmal.observable.cell.mutableCell
import world.phantasmal.web.application.Application
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.persistence.LocalStorageKeyValueStore
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
import world.phantasmal.web.core.stores.ApplicationUrl
import world.phantasmal.web.externals.three.WebGLRenderer
@ -58,6 +59,7 @@ private fun init(): Disposable {
disposer.add(
Application(
rootElement,
LocalStorageKeyValueStore(),
AssetLoader(httpClient),
disposer.add(HistoryApplicationUrl()),
::createThreeRenderer,

View File

@ -14,6 +14,7 @@ import world.phantasmal.web.application.widgets.MainContentWidget
import world.phantasmal.web.application.widgets.NavigationWidget
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.persistence.KeyValueStore
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
import world.phantasmal.web.core.stores.ApplicationUrl
import world.phantasmal.web.core.stores.UiStore
@ -25,6 +26,7 @@ import world.phantasmal.webui.dom.disposableListener
class Application(
rootElement: HTMLElement,
keyValueStore: KeyValueStore,
assetLoader: AssetLoader,
applicationUrl: ApplicationUrl,
createThreeRenderer: (HTMLCanvasElement) -> DisposableThreeRenderer,
@ -34,7 +36,7 @@ class Application(
addDisposables(
// Disable native undo/redo.
document.disposableListener("beforeinput", ::beforeInput),
// Work-around for FireFox:
// Disable native undo/redo in FireFox.
document.disposableListener("keydown", ::keydown),
// Disable native drag-and-drop to avoid users dragging in unsupported file formats and
@ -50,8 +52,8 @@ class Application(
// The various tools Phantasmal World consists of.
val tools: List<PwTool> = listOf(
addDisposable(Viewer(assetLoader, uiStore, createThreeRenderer)),
addDisposable(QuestEditor(assetLoader, uiStore, createThreeRenderer)),
addDisposable(HuntOptimizer(assetLoader, uiStore)),
addDisposable(QuestEditor(keyValueStore, assetLoader, uiStore, createThreeRenderer)),
addDisposable(HuntOptimizer(keyValueStore, assetLoader, uiStore)),
)
// Controllers.

View File

@ -0,0 +1,28 @@
package world.phantasmal.web.core.persistence
import kotlinx.browser.localStorage
interface KeyValueStore {
suspend fun get(key: String): String?
suspend fun put(key: String, value: String)
}
class LocalStorageKeyValueStore : KeyValueStore {
override suspend fun get(key: String): String? =
localStorage.getItem(key)
override suspend fun put(key: String, value: String) {
localStorage.setItem(key, value)
}
}
class MemoryKeyValueStore : KeyValueStore {
private val map = mutableMapOf<String, String>()
override suspend fun get(key: String): String? =
map[key]
override suspend fun put(key: String, value: String) {
map[key] = value
}
}

View File

@ -1,6 +1,5 @@
package world.phantasmal.web.core.persistence
import kotlinx.browser.localStorage
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
@ -9,7 +8,7 @@ import world.phantasmal.web.core.models.Server
private val logger = KotlinLogging.logger {}
abstract class Persister {
abstract class Persister(private val store: KeyValueStore) {
private val format = Json {
classDiscriminator = "#type"
ignoreUnknownKeys = true
@ -23,7 +22,7 @@ abstract class Persister {
@Suppress("RedundantSuspendModifier")
protected suspend fun <T> persist(key: String, data: T, serializer: KSerializer<T>) {
try {
localStorage.setItem(key, format.encodeToString(serializer, data))
store.put(key, format.encodeToString(serializer, data))
} catch (e: Throwable) {
logger.error(e) { "Couldn't persist ${key}." }
}
@ -44,7 +43,7 @@ abstract class Persister {
@Suppress("RedundantSuspendModifier")
protected suspend fun <T> load(key: String, serializer: KSerializer<T>): T? =
try {
val json = localStorage.getItem(key)
val json = store.get(key)
json?.let { format.decodeFromString(serializer, it) }
} catch (e: Throwable) {
logger.error(e) { "Couldn't load ${key}." }

View File

@ -3,6 +3,7 @@ package world.phantasmal.web.huntOptimizer
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.persistence.KeyValueStore
import world.phantasmal.web.core.stores.ItemDropStore
import world.phantasmal.web.core.stores.ItemTypeStore
import world.phantasmal.web.core.stores.UiStore
@ -16,6 +17,7 @@ import world.phantasmal.webui.DisposableContainer
import world.phantasmal.webui.widgets.Widget
class HuntOptimizer(
private val keyValueStore: KeyValueStore,
private val assetLoader: AssetLoader,
private val uiStore: UiStore,
) : DisposableContainer(), PwTool {
@ -25,8 +27,8 @@ class HuntOptimizer(
val itemTypeStore = addDisposable(ItemTypeStore(assetLoader))
// Persistence
val huntMethodPersister = HuntMethodPersister()
val wantedItemPersister = WantedItemPersister(itemTypeStore)
val huntMethodPersister = HuntMethodPersister(keyValueStore)
val wantedItemPersister = WantedItemPersister(keyValueStore, itemTypeStore)
// Stores
val huntMethodStore =

View File

@ -1,12 +1,13 @@
package world.phantasmal.web.huntOptimizer.persistence
import world.phantasmal.web.core.models.Server
import world.phantasmal.web.core.persistence.KeyValueStore
import world.phantasmal.web.core.persistence.Persister
import world.phantasmal.web.huntOptimizer.models.HuntMethodModel
import kotlin.time.Duration
import kotlin.time.DurationUnit.HOURS
class HuntMethodPersister : Persister() {
class HuntMethodPersister(keyValueStore: KeyValueStore) : Persister(keyValueStore) {
suspend fun persistMethodUserTimes(huntMethods: List<HuntMethodModel>, server: Server) {
val userTimes = mutableMapOf<String, Double>()

View File

@ -1,12 +1,17 @@
package world.phantasmal.web.huntOptimizer.persistence
import world.phantasmal.web.core.models.Server
import world.phantasmal.web.core.persistence.KeyValueStore
import world.phantasmal.web.core.persistence.Persister
import world.phantasmal.web.core.stores.ItemTypeStore
import world.phantasmal.web.shared.dto.WantedItemDto
import world.phantasmal.web.huntOptimizer.models.WantedItemModel
import world.phantasmal.web.shared.dto.WantedItemDto
class WantedItemPersister(
keyValueStore: KeyValueStore,
private val itemTypeStore: ItemTypeStore,
) : Persister(keyValueStore) {
class WantedItemPersister(private val itemTypeStore: ItemTypeStore) : Persister() {
suspend fun persistWantedItems(wantedItems: List<WantedItemModel>, server: Server) {
persistForServer(server, WANTED_ITEMS_KEY, wantedItems.map {
WantedItemDto(it.itemType.id, it.amount.value)

View File

@ -6,6 +6,7 @@ import org.w3c.dom.HTMLCanvasElement
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.persistence.KeyValueStore
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.core.undo.UndoManager
@ -25,6 +26,7 @@ import world.phantasmal.webui.dom.disposableListener
import world.phantasmal.webui.widgets.Widget
class QuestEditor(
private val keyValueStore: KeyValueStore,
private val assetLoader: AssetLoader,
private val uiStore: UiStore,
private val createThreeRenderer: (HTMLCanvasElement) -> DisposableThreeRenderer,
@ -38,7 +40,7 @@ class QuestEditor(
val entityAssetLoader = addDisposable(EntityAssetLoader(assetLoader))
// Persistence
val questEditorUiPersister = QuestEditorUiPersister()
val questEditorUiPersister = QuestEditorUiPersister(keyValueStore)
// Undo
val undoManager = UndoManager()

View File

@ -1,10 +1,11 @@
package world.phantasmal.web.questEditor.persistence
import world.phantasmal.web.core.controllers.*
import world.phantasmal.web.core.persistence.KeyValueStore
import world.phantasmal.web.core.persistence.Persister
import world.phantasmal.web.shared.dto.*
class QuestEditorUiPersister : Persister() {
class QuestEditorUiPersister(keyValueStore: KeyValueStore) : Persister(keyValueStore) {
// TODO: Throttle this method.
suspend fun persistLayoutConfig(config: DockedItem) {
persist(LAYOUT_CONFIG_KEY, toDto(config))

View File

@ -2,6 +2,7 @@ package world.phantasmal.web.application
import kotlinx.browser.document
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.persistence.MemoryKeyValueStore
import world.phantasmal.web.test.TestApplicationUrl
import world.phantasmal.web.test.WebTestContext
import world.phantasmal.web.test.WebTestSuite
@ -30,8 +31,10 @@ class ApplicationTests : WebTestSuite {
private fun WebTestContext.initialization_and_shutdown_succeeds(url: String) {
components.applicationUrl = TestApplicationUrl(url)
disposer.add(
Application(
keyValueStore = MemoryKeyValueStore(),
rootElement = document.body!!,
assetLoader = components.assetLoader,
applicationUrl = components.applicationUrl,

View File

@ -1,6 +1,7 @@
package world.phantasmal.web.huntOptimizer
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.persistence.MemoryKeyValueStore
import world.phantasmal.web.test.TestApplicationUrl
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test
@ -10,7 +11,10 @@ class HuntOptimizerTests : WebTestSuite {
fun initialization_and_shutdown_should_succeed_without_throwing() = test {
components.applicationUrl = TestApplicationUrl("/${PwToolType.HuntOptimizer}")
val huntOptimizer = disposer.add(HuntOptimizer(components.assetLoader, components.uiStore))
val huntOptimizer = disposer.add(
HuntOptimizer(MemoryKeyValueStore(), components.assetLoader, components.uiStore)
)
disposer.add(huntOptimizer.initialize())
}
}

View File

@ -1,6 +1,7 @@
package world.phantasmal.web.questEditor
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.persistence.MemoryKeyValueStore
import world.phantasmal.web.test.TestApplicationUrl
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test
@ -11,8 +12,14 @@ class QuestEditorTests : WebTestSuite {
components.applicationUrl = TestApplicationUrl("/${PwToolType.QuestEditor}")
val questEditor = disposer.add(
QuestEditor(components.assetLoader, components.uiStore, components.createThreeRenderer)
QuestEditor(
MemoryKeyValueStore(),
components.assetLoader,
components.uiStore,
components.createThreeRenderer,
)
)
disposer.add(questEditor.initialize())
}
}

View File

@ -13,6 +13,7 @@ class ViewerTests : WebTestSuite {
val viewer = disposer.add(
Viewer(components.assetLoader, components.uiStore, components.createThreeRenderer)
)
disposer.add(viewer.initialize())
}
}