-
-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add LocalStorageKey; rework LocalStorage API #30
base: main
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #30 +/- ##
==========================================
+ Coverage 88.75% 90.04% +1.29%
==========================================
Files 6 8 +2
Lines 471 552 +81
==========================================
+ Hits 418 497 +79
- Misses 53 55 +2
Continue to review full report in Codecov by Sentry.
|
(the second part is still work in progress)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm not overly experienced when it comes to using Combine, so please let me know whether what i'm doing here makes sense / is the correct approach
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks alright to me, even tho I'm also rather inexperienced with Combine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the great PR @lukaskollmer! 🚀
Mostly had some smaller nits and long-shot ideas. What's important is to update the rest of the documentation.
And yes, a similar PR in the SecureStorage would be great as well!
} | ||
|
||
|
||
struct LocalStorageLiveUpdateTestView: View { // swiftlint:disable:this file_types_order |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool test, thanks!
let numbers = (0..<17).map { _ in Int.random(in: 0..<5) } | ||
for number in numbers { | ||
app.buttons["\(number)"].tap() | ||
XCTAssertTrue(app.staticTexts.matching(NSPredicate(format: "label MATCHES %@", "number.*\(number)")).element.waitForExistence(timeout: 0.5)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The timeouts might be a bit low for the CI (we usually have around 2 sec), but we can evolve that if needed in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i added the small delay since not having any delay would in some cases not catch the update (afaik the update happens on the next run loop invocation, so there is a small delay we need to account for)
### Defining Storage Keys | ||
|
||
`LocalStorage` uses unique ``LocalStorageKey``s to . | ||
|
||
You define storage keys by placing a static non-computed properties of type ``LocalStorageKey`` into an extension on the ``LocalStorageKeys`` type: | ||
|
||
```swift | ||
extension LocalStorageKeys { | ||
static let note = LocalStorageKey | ||
} | ||
``` | ||
|
||
|
||
### Storing and Loading Data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the updated docs! 🚀 I feel there are quite some other doc parts that we need to updated in the README and DocC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch; i'll look over the docs and make sure everything is in sync and up to date
encode: @Sendable @escaping (Value) throws -> Data, | ||
decode: @Sendable @escaping (Data) throws -> Value? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool idea, I like it!
let setting: LocalStorageSetting | ||
let encode: @Sendable (Value) throws -> Data | ||
let decode: @Sendable (Data) throws -> Value? | ||
private let lock = RWLock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just as a long shot: Should we aim to move away from manual locking and go towards an async/await actor-based implementation here? This would of course then also propagate to the public layer, which probably is not wanted. Might be overkill, just putting it out there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that was @PSchmiedmayer's idea as well, but there's, from what i can tell, some issues with that approach:
- with actors, we wouldn't be able to allow concurrent reads while enforcing exclusive writes (which currently is allowed).
whether this is something we actually need is a different question of course (the answer is almost definitely no, but i'd argue it does feel more elegant this way) - were we to use actors, we'd need to make all store() / load() calls async (that'd be the entire point, of course), which might be overkill, since the likelihood of parallel reads is already pretty low (plus, with the current implementation parallel reads would be fine anyway), and parallel writes are already synchronized currently.
- more importantly, we wouldn't be able to have a property-wrapper for accessing storage entries anymore, since the
wrappedValue
can't be async.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep I share these concerns as well, I think the downsides of actor isolation outway the benefits in this case, so from my side I'm fine with keeping the RWLock
, even tho it doesn't feel super swifty.
|
||
|
||
/// Access ``LocalStorage`` entries within a SwiftUI View. | ||
@propertyWrapper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea to introduce that! 🚀
Would be great to get a small usage doc in here for the wrapper as well as writing down the need for the LocalStorage
module in the env.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lukaskollmer Might make sense to define the wrapper like this, just on a View
?
https://github.com/StanfordSpezi/SpeziLLM/blob/26b1e07756556b1b329f3a9a3267a5f99c0c2e78/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper.swift#L83
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure what you mean / how that would would work. @Property
is a property wrapper which gets its value externally injected (as part of handling the LLM function call), which isn't something we can do with SwiftUI (if that's what you're talking about).
or are you referring to some other aspect of @Parameter
?
/// If the closure returns `nil`, the entry will be removed from the `LocalStorage`. | ||
/// | ||
/// - throws: if `transform` throws, | ||
public func modify<Value>(_ key: LocalStorageKey<Value>, transform: (Value?) throws -> Value?) throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Would be great to test this function
if let value { | ||
try storeImp(value, for: key) | ||
} else { | ||
try delete(key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Would be great to test this branch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks alright to me, even tho I'm also rather inexperienced with Combine.
@LocalStorageEntry(.number) private var number | ||
|
||
var body: some View { | ||
LabeledContent("number", value: number.map(String.init) ?? "–") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe test a small write to the property wrapper as well?
who would've thought testing the code would reveal the bugs?
add LocalStorageKey; rework LocalStorage API
♻️ Current situation & Problem
LocalStorage
currently identifies entries in the storage via either optional, raw untyped strings, or via the static type of the to-be-stored/loaded value.While this approach does lead to a nice-looking API, it is problematic for several reasons:
LocalStorage
entry)store()
as well as calls toread()
, means that it is easy to mismatch these parametersPropertyListEncoder
when storing some value into theLocalStorage
, you need to make sure that you also use the matchingPropertyListDecoder
when reading the stored value.store()
and the decoder inread()
to theJSONEncoder
(and `JSONDecoder, respectively), makes it too easy to miss the parameter somewherestorageSetting
parameter: all matching calls tostore()
andread()
that operate on the same storage key must specify the same storage setting.store()
/read()
that operate on completely different types (potentially even in completely different modules!) end up operating on the same LocalStorage entry, because the unqualified typename will be the same in both casesThis PR attempts to address these issues, by restructuring and redesigning the
LocalStorage
API:store()
/load()
callsLocalStorageKey<Value>
type, which are intended to be long-lived objects that are used to access theLocalStorage
LocalStorageKey
encapsulates all information required to store data into the storage / load data from itLocalStorageKey
instance, we can ensure that every store/load for the same entry will use matching decoders/encoders, and will use the same storageSetting.LocalStorageKey
also acts as a read-write lock when accessing the storage entry identified by itLocalStorageKey
-based API also makes it easier to support storing additional currently not supported types, such as e.g.NSSecureCoding
-compliant typesCodableWithConfiguration
, which previously was supported but (AFAICT) not used.SpeziSecureStorage
is currently untouched; it might make sense to have something similar there as well, but it'd of course require a separate, dedicated key type and would be out-of-scope for this PR.@LocalStorageEntry
, which makes it easier to use theLocalStorage
API from within a SwiftUI View, and is e.g. able to observe a key's value and update the view in response.⚙️ Release Notes
LocalStorage
API:LocalStorageKey
s, which define an entry in the storage.LocalStorageKey
s need to explicitly specify their underlying raw keys (a String), and are encouraged to use reverse-DNS-notation in order to avoid collisions📚 Documentation
The documentation has been adapted to the new API.
✅ Testing
The tests have been adapted to the new API.
📝 Code of Conduct & Contributing Guidelines
By submitting creating this pull request, you agree to follow our Code of Conduct and Contributing Guidelines: