-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
AbstractWebStorage.scala
166 lines (126 loc) · 5.32 KB
/
AbstractWebStorage.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package japgolly.webapputil.webstorage
import japgolly.microlibs.utils.SetOnceVar
import japgolly.scalajs.react.{Callback, CallbackTo, Reusability}
import japgolly.univeq.UnivEq
import org.scalajs.dom.{Storage => StorageJs}
import scala.scalajs.js
trait AbstractWebStorage {
import AbstractWebStorage.{Key, Value}
def clear: Callback
def getItem(key: Key): CallbackTo[Option[Value]]
def removeItem(key: Key): Callback
def setItem(key: Key, data: Value): Callback
def getLength: CallbackTo[Int]
def getKey(index: Int): CallbackTo[Option[Key]]
final def setOrRemoveItem(key: Key, data: Option[Value]): Callback =
data match {
case Some(v) => setItem(key, v)
case None => removeItem(key)
}
}
object AbstractWebStorage {
def apply(s: StorageJs): AbstractWebStorage =
new Real(s)
// Keep this as a def. It might be undefined at first, then later user grants access and it becomes available.
// Is that a valid scenario? I don't know but may as well support it if possible.
def local(): Option[AbstractWebStorage] =
try {
// Some platforms provide a localStorage instance but attempting to use it results in an exception. Eg:
//
// - "SecurityError: The operation is insecure"
// UserAgent: Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Mobile/15E148 Safari/604.1
//
// - "SecurityError: Failed to read the 'localStorage' property from 'Window': Access is denied for this document."
// UserAgent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36
//
// Here we take the same approach as store.js and test usage.
// See: https://github.com/marcuswestin/store.js/blob/b8e22fea8738fc19da4d9e7dbf1cda6e5185c481/src/store-engine.js#L108-L117
//
def test(s: StorageJs): Boolean = {
val k = "_______japgolly_0abec9a2-6e88-42ae-a2b8-73b2a904f8a0"
val v = "zz"
try {
s.setItem(k, v)
s.getItem(k) == v
} finally
s.removeItem(k)
}
js.Dynamic.global.localStorage.asInstanceOf[js.UndefOr[StorageJs]]
.toOption
.flatMap(Option(_))
.filter(test)
.map(ls => localStorageInstance.getOrSet(new Real(ls)))
} catch {
case _: Throwable => None
}
private val localStorageInstance = SetOnceVar[Real]
def localOrEmpty(): AbstractWebStorage =
local().getOrElse(AlwaysEmpty)
implicit def reusability: Reusability[AbstractWebStorage] =
Reusability.byRef
final case class Key(value: String) extends AnyVal
final case class Value(value: String) extends AnyVal {
def mod(f: String => String): Value =
Value(f(value))
}
implicit def univEqKey: UnivEq[Key] = UnivEq.derive
implicit def univEqValue: UnivEq[Value] = UnivEq.derive
// ===================================================================================================================
object AlwaysEmpty extends AbstractWebStorage {
override def clear: Callback =
Callback.empty
override def getItem(key: Key): CallbackTo[Option[Value]] =
CallbackTo.pure(None)
override def removeItem(key: Key): Callback =
Callback.empty
override def setItem(key: Key, data: Value): Callback =
Callback.empty
override def getLength: CallbackTo[Int] =
CallbackTo.pure(0)
override def getKey(index: Int): CallbackTo[Option[Key]] =
CallbackTo.pure(None)
}
// ===================================================================================================================
private final class Real(storageJs: StorageJs) extends AbstractWebStorage {
override def clear: Callback =
Callback(storageJs.clear())
override def getItem(key: Key): CallbackTo[Option[Value]] =
CallbackTo {
storageJs.getItem(key.value) match {
case null => None
case v => Some(Value(v))
}
}
override def removeItem(key: Key): Callback =
Callback(storageJs.removeItem(key.value))
override def setItem(key: Key, data: Value): Callback =
Callback(storageJs.setItem(key.value, data.value))
override def getLength: CallbackTo[Int] =
CallbackTo(storageJs.length)
override def getKey(index: Int): CallbackTo[Option[Key]] =
CallbackTo {
storageJs.key(index) match {
case null => None
case k => Some(Key(k))
}
}
}
// ===================================================================================================================
def inMemory() =
new InMemory
final class InMemory extends AbstractWebStorage {
private var state = Map.empty[String, String]
override def clear: Callback =
Callback {state = Map.empty}
override def getItem(key: Key): CallbackTo[Option[Value]] =
CallbackTo(state.get(key.value).map(Value.apply))
override def removeItem(key: Key): Callback =
Callback {state -= key.value}
override def setItem(key: Key, data: Value): Callback =
Callback {state = state.updated(key.value, data.value)}
override def getLength: CallbackTo[Int] =
CallbackTo(state.size)
override def getKey(index: Int): CallbackTo[Option[Key]] =
CallbackTo(state.iterator.drop(index).nextOption().map(e => Key(e._1)))
}
}