-
Notifications
You must be signed in to change notification settings - Fork 48
feat(sds): persistent storage for history #2741
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
base: master
Are you sure you want to change the base?
Conversation
| * | ||
| * If no storage backend is available, this behaves like {@link MemLocalHistory}. | ||
| */ | ||
| export class PersistentHistory extends MemLocalHistory { |
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 are you extending another class instead of the interface? Avoid abstractions as a rule of thumb
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.
extended so that we could get the implementations for the following functions without needing to reimplement:
push(...items: ContentMessage[]): number;
some(
predicate: (
value: ContentMessage,
index: number,
array: ContentMessage[]
) => unknown,
thisArg?: any
): boolean;
slice(start?: number, end?: number): ContentMessage[];
find(
predicate: (
value: ContentMessage,
index: number,
obj: ContentMessage[]
) => unknown,
thisArg?: any
): ContentMessage | undefined;
findIndex(
predicate: (
value: ContentMessage,
index: number,
obj: ContentMessage[]
) => unknown,
thisArg?: any
): number;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.
Yes but you already know you want to get rid of some of that with the optimisation so I would avoid the abstraction usage.
You can also extract those logic if necessary. Saving code is not always the best way forward. See my new comments where naming is confusing.
| this.restore(); | ||
| } | ||
|
|
||
| public override push(...items: ContentMessage[]): number { |
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.
Yeah, don't use abstraction. Its not worth the indirection you are bringing in
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.
switched to composition!
| } | ||
|
|
||
| private persist(): void { | ||
| if (!this.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.
Hum, does it make sense for it to be constructed without 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.
we want the class to behave like MemLocalHistory if no storage is provided, right?
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.
or perhaps, that should be handled one level above where MessageChannel chooses between MemLocalHistory and PersistentHistory -- suggestions?
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.
we want the class to behave like MemLocalHistory if no storage is provided, right?
So you are saying that if we instantiate a persistent local history, but there is no persistence to it, then we want it to behave like memory local history? Think about the footgun you are setting up for developers.
If there is no way to persist the history, then the class handling persisting history should not be instantiable.
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.
Indeed:
if (storage instanceof PersistentStorage) {
this.storage = storage;
log.info("Using explicit persistent storage");
} else if (typeof storage === "string") {
this.storage = PersistentStorage.create(storage);
log.info("Creating persistent storage for channel", storage);
} else {
this.storage = undefined;
log.info("Using in-memory storage");
}d7aa504 to
92dd0ba
Compare
size-limit report 📦
|
|
Restructured the PR: |
df7d56b to
3e3c511
Compare
| export interface ILocalHistory { | ||
| length: number; | ||
| push(...items: ContentMessage[]): number; | ||
| some( | ||
| predicate: ( | ||
| value: ContentMessage, | ||
| index: number, | ||
| array: ContentMessage[] | ||
| ) => unknown, | ||
| thisArg?: any | ||
| ): boolean; | ||
| slice(start?: number, end?: number): ContentMessage[]; | ||
| find( | ||
| predicate: ( | ||
| value: ContentMessage, | ||
| index: number, | ||
| obj: ContentMessage[] | ||
| ) => unknown, | ||
| thisArg?: any | ||
| ): ContentMessage | undefined; | ||
| findIndex( | ||
| predicate: ( | ||
| value: ContentMessage, | ||
| index: number, | ||
| obj: ContentMessage[] | ||
| ) => unknown, | ||
| thisArg?: any | ||
| ): number; | ||
| } |
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.
Does not seem to be the right place to define this interface. Also, it is the same as before (Pick<Array...).
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.
tackled here: #2745
fryorcraken
left a comment
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 naming is very confusing. It is unclear what is actually related to local history, vs the local storage interfaces.
Not sure how the message channel is supposed to use the persistent storage when there is no persistent storage implementing ILocalHistory.
| * at next push. | ||
| */ | ||
| export class MemLocalHistory { | ||
| export interface ILocalHistory { |
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.
define the interface alongside the class that needs this interface, aka, message channel.
It is odd to define the interface along side one of the implementations.
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.
ILocalHistory is being implemented by LocalHistory (prev MemLocalHistory), so it needs it as well. Would you suggest moving it to MessageChannel?
Since the concept for LocalHistory belongs in this file, I believed it's good design to keep it close to the implementation. The interface exists because of the class
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.
Based on your observation that it's only being used once, removed it.
| } | ||
|
|
||
| export type MemLocalHistoryOptions = { | ||
| storage?: ChannelId | PersistentStorage; |
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.
What are you doing here? Why would you use persistent storage for the Memory implementation?
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.
Ok, no I got it, see my recent comment on the PR.
I would not have this kind of type switching here.
You can have a storagePrefix string that is applied in any case (whether you use browser localStorage or fs). It is relevant to both.
Then, for PersistentStorage, could we instead use the package.json browser feature and have 2 files:
browser-localStorage.ts
localStorage.ts
Both of them would expert a LocalStorage (what you did with PersistentStorage class, except that for the browser one, the LocalStorage class is just a thin wrap on the browser localStorage
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.
Okay, interesting, that's probably a neater solution.
| this.incomingBuffer = []; | ||
| this.localHistory = localHistory; | ||
| this.localHistory = | ||
| localHistory ?? new MemLocalHistory({ storage: channelId }); |
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.
Considering that this is not an API we actually want to expose, (ReliableChannel is), then it's fine to not have default.
| export interface HistoryStorage { | ||
| getItem(key: string): string | null; | ||
| setItem(key: string, value: string): void; | ||
| removeItem(key: string): void; |
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.
what is that? it does not seem to be "History" right? It's supposed to be the interface for local storage, right? Call it ILocalStorage then
|
After more review, I now better understand what is done here: This is not the architecture I originally had in mind, which is fine. It just means that I did some pre-optimisation that now needs to be trashed. In this proposed architecture, there is only one implementation of
Then the last part |
434728f to
96ec51f
Compare
Ah, I see where the confusion came from. Thanks for the diagram (note to self to include it wherever possible), I can see how it may have been confusing since the structure changed.
Agreed, see my comment here: #2741 (comment)
Valid point, thanks! One caveat: the class is more than just "local storage" as it allows users to pass their custom storage providers, so |
…istentHistory` to use composition over inheritance for history management.
…e key generation.
…ifying history storage initialization to default to `localStorage`
…mproved compatibility.
…in message channels
…d refactor `MemLocalHistory` to support optional persistent storage.
… persistence, updating LocalHistory to utilize a unified Storage interface and enhancing tests for localStorage functionality
75e8e12 to
e396c3b
Compare
8503315 to
ab6d3cf
Compare
Problem / Description
Message history is currently stored only in memory, causing SDS to start recovery afresh every time.
Solution
This PR introduces persistent message history that survives application restarts using localStorage (browser), with an option to provide custom storage providers.
Changes
MemLocalHistorynow optionally sets upStoragelocalStorageusage in browsers with serialisation supportMessageChannelwith persistent storage support, by defaultNotes
Checklist