feat(history): introduce ILocalHistory interface and refactor PersistentHistory to use composition over inheritance for history management.

This commit is contained in:
Danish Arora 2025-11-20 16:11:12 -05:00
parent 454dc4a93b
commit ca24c09688
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
2 changed files with 81 additions and 8 deletions

View File

@ -17,7 +17,37 @@ export const DEFAULT_MAX_LENGTH = 10_000;
* If an array of items longer than `maxLength` is pushed, dropping will happen
* at next push.
*/
export class MemLocalHistory {
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;
}
export class MemLocalHistory implements ILocalHistory {
private items: ContentMessage[] = [];
/**

View File

@ -1,6 +1,6 @@
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { MemLocalHistory } from "./mem_local_history.js";
import { ILocalHistory, MemLocalHistory } from "./mem_local_history.js";
import { ChannelId, ContentMessage, HistoryEntry } from "./message.js";
export interface HistoryStorage {
@ -38,31 +38,73 @@ const HISTORY_STORAGE_PREFIX = "waku:sds:history:";
*
* If no storage backend is available, this behaves like {@link MemLocalHistory}.
*/
export class PersistentHistory extends MemLocalHistory {
export class PersistentHistory implements ILocalHistory {
private readonly storage?: HistoryStorage;
private readonly storageKey: string;
private readonly memory: MemLocalHistory;
public constructor(options: PersistentHistoryOptions) {
super();
this.memory = new MemLocalHistory();
this.storage = options.storage ?? getDefaultHistoryStorage();
this.storageKey =
options.storageKey ?? `${HISTORY_STORAGE_PREFIX}${options.channelId}`;
this.restore();
}
public override push(...items: ContentMessage[]): number {
const length = super.push(...items);
public get length(): number {
return this.memory.length;
}
public push(...items: ContentMessage[]): number {
const length = this.memory.push(...items);
this.persist();
return length;
}
public some(
predicate: (
value: ContentMessage,
index: number,
array: ContentMessage[]
) => unknown,
thisArg?: any
): boolean {
return this.memory.some(predicate, thisArg);
}
public slice(start?: number, end?: number): ContentMessage[] {
return this.memory.slice(start, end);
}
public find(
predicate: (
value: ContentMessage,
index: number,
obj: ContentMessage[]
) => unknown,
thisArg?: any
): ContentMessage | undefined {
return this.memory.find(predicate, thisArg);
}
public findIndex(
predicate: (
value: ContentMessage,
index: number,
obj: ContentMessage[]
) => unknown,
thisArg?: any
): number {
return this.memory.findIndex(predicate, thisArg);
}
private persist(): void {
if (!this.storage) {
return;
}
try {
const payload = JSON.stringify(
this.slice(0).map(serializeContentMessage)
this.memory.slice(0).map(serializeContentMessage)
);
this.storage.setItem(this.storageKey, payload);
} catch {
@ -86,7 +128,7 @@ export class PersistentHistory extends MemLocalHistory {
.map(deserializeContentMessage)
.filter((message): message is ContentMessage => Boolean(message));
if (messages.length) {
super.push(...messages);
this.memory.push(...messages);
}
} catch {
try {
@ -153,6 +195,7 @@ const deserializeContentMessage = (
BigInt(record.lamportTimestamp),
fromHex(record.bloomFilter),
content,
[],
fromHex(record.retrievalHint)
);
} catch {