-
Notifications
You must be signed in to change notification settings - Fork 42
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
Implementing beforeRead and beforeWrite #250
Merged
Merged
Changes from 6 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
f7c1927
Make persisted ignore events when value is null
bertmad3400 32553f3
Add beforeWrite and beforeRead methods
bertmad3400 77ac10d
Implement tests for beforeRead and beforeWrite
bertmad3400 15a0a88
Fix spelling mistakes
bertmad3400 49ab7f5
Add syntax example for beforeRead and beforeWrite to readme
bertmad3400 ff269f0
Add cancel operation to beforeRead and beforeWrite
bertmad3400 9e0c092
Add seperate store and serializer types
bertmad3400 81a918c
Fix single type-error
bertmad3400 b3b985f
Update README
bertmad3400 f89f201
Revert implementation, documentation and testing for cancelling befor…
bertmad3400 007544c
Remove mention of cancel param from readme
bertmad3400 cb0a317
Set StoreType as the default shape for SerializerType
bertmad3400 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,20 +14,22 @@ const stores: Stores = { | |
session: {} | ||
} | ||
|
||
export interface Serializer<T> { | ||
parse(text: string): T | ||
stringify(object: T): string | ||
export interface Serializer { | ||
parse(text: string): any | ||
stringify(object: any): string | ||
} | ||
|
||
export type StorageType = 'local' | 'session' | ||
|
||
export interface Options<T> { | ||
serializer?: Serializer<T> | ||
serializer?: Serializer | ||
storage?: StorageType, | ||
syncTabs?: boolean, | ||
onError?: (e: unknown) => void | ||
onWriteError?: (e: unknown) => void | ||
onParseError?: (newValue: string | null, e: unknown) => void | ||
beforeRead?: <S extends symbol>(val: any, cancel: S) => T | S | ||
beforeWrite?: <S extends symbol>(val: T, cancel: S) => any | S | ||
} | ||
|
||
function getStorage(type: StorageType) { | ||
|
@@ -47,45 +49,63 @@ export function persisted<T>(key: string, initialValue: T, options?: Options<T>) | |
const syncTabs = options?.syncTabs ?? true | ||
const onWriteError = options?.onWriteError ?? options?.onError ?? ((e) => console.error(`Error when writing value from persisted store "${key}" to ${storageType}`, e)) | ||
const onParseError = options?.onParseError ?? ((newVal, e) => console.error(`Error when parsing ${newVal ? '"' + newVal + '"' : "value"} from persisted store "${key}"`, e)) | ||
|
||
const beforeRead = options?.beforeRead ?? ((val) => val as T) | ||
const beforeWrite = options?.beforeWrite ?? ((val) => val as any) | ||
|
||
const browser = typeof (window) !== 'undefined' && typeof (document) !== 'undefined' | ||
const storage = browser ? getStorage(storageType) : null | ||
|
||
function updateStorage(key: string, value: T) { | ||
const cancel = Symbol("cancel") | ||
const newVal = beforeWrite(value, cancel) | ||
if (newVal === cancel) return | ||
|
||
try { | ||
storage?.setItem(key, serializer.stringify(value)) | ||
storage?.setItem(key, serializer.stringify(newVal)) | ||
} catch (e) { | ||
onWriteError(e) | ||
} | ||
} | ||
|
||
function maybeLoadInitial(): T { | ||
const json = storage?.getItem(key) | ||
|
||
if (json) { | ||
function serialize(json: any) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason for extracting this function? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
try { | ||
return <T>serializer.parse(json) | ||
} catch (e) { | ||
onParseError(json, e) | ||
} | ||
} | ||
const json = storage?.getItem(key) | ||
if (json == null) return initialValue | ||
|
||
return initialValue | ||
const serialized = serialize(json) | ||
if (serialized == null) return initialValue | ||
|
||
const cancel = Symbol("cancel") | ||
const newVal = beforeRead(serialized, cancel) | ||
if (newVal === cancel) return initialValue | ||
return newVal | ||
} | ||
|
||
if (!stores[storageType][key]) { | ||
const initial = maybeLoadInitial() | ||
const store = internal(initial, (set) => { | ||
if (browser && storageType == 'local' && syncTabs) { | ||
const handleStorage = (event: StorageEvent) => { | ||
if (event.key === key) { | ||
if (event.key === key && event.newValue) { | ||
let newVal: any | ||
try { | ||
newVal = event.newValue ? serializer.parse(event.newValue) : null | ||
newVal = serializer.parse(event.newValue) | ||
} catch (e) { | ||
onParseError(event.newValue, e) | ||
return | ||
} | ||
set(newVal) | ||
const cancel = Symbol("cancel") | ||
const processedVal = beforeRead(newVal, cancel) | ||
if (processedVal === cancel) return | ||
|
||
set(processedVal) | ||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Why the need for a symbol?
Can't
beforeWrite
just return a 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.
As described before, the symbol here is used to cancel the write operation. Imagine a case where the beforeWrite function would want to cancel the writing to localStorage (this could maybe be because of invalid data, because of cases like #217, or maybe something else). As we don't know of the shape of the store data, beforeWrite should be able to return any kind of value (primitives (numbers, strings, etc.), objects, arrays and even null/undefined) and have it written to localStorage. As such, if we want to implement cancel by return value, we have to create a value that we know is unique, and that we know that beforeWrite couldn't possibly return, unless it's explicitly our cancel value, which is why symbols are used (as this is their entire purpose). The only other way of implementing a cancel operation would be to passe in a cancel function that the when called would cancel the write operation, but not only is this more code in the library, but I also believe it to be more confusing for the user, as the return value suddenly have no effect then.
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 still unclear on why
beforeWrite()
should be allowed to cancel and how often this is needed. (Just trying to keep implementation and docs simple)Cancelation means "keep the last value", but we could do that with a return statement. for example
But still unclear on why anyone would want to cancel a write, and let the store's state get out of sync with local storage.
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.
Se there's a couple of sides to this:
Ultimately, it would be up to you whether we want to keep this functionality or not (I can just remove it if not desired), but I would personally recommend for it, both due to the previously mentioned cases (where I don't see a simple solution without it), and because I believe - from a developer perspective - that it's a simple and intuitive solution. Just returning the existingVal to beforeRead would mean that the input signatures for beforeRead and beforeWrite would be different, and will - in my mind - definitely at some point lead to developers asking us "It's great I can prevent updates through beforeRead, but how do I do the same for beforeWrite?". I think the solution of keeping the input signature, and the mechanism for cancelling (by returning the cancel param) the same for both functions is the simplest for developers to understand and get used to.
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.
To clarify, I was questioning why cancel is needed for beforeRead & before write.
I think it's overkill for this library right now.
As with all decisions, I'm open to changing it if more people ask for it.
It's important when building things to target the 80% - what most people want. If we try to target 100% this project will get very complicated, and it will impact the users and the maintenance.
I appreciate your work on this, but I think we should not worry about edge cases untill we hear many people need this.
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.
Got it. While I'm still not sure I agree, I definitely see your point, and will just go ahead and remove this functionality.