|
| 1 | +import type { Detail } from "jsr:@vim-fall/core@^0.3.0/item"; |
| 2 | +import { brotli } from "jsr:@deno-library/compress@^0.5.6"; |
| 3 | + |
| 4 | +import type { PickerContext } from "./picker.ts"; |
| 5 | + |
| 6 | +/** |
| 7 | + * In-memory storage for compressed picker sessions. |
| 8 | + * Sessions are stored in chronological order (oldest first). |
| 9 | + */ |
| 10 | +// deno-lint-ignore no-explicit-any |
| 11 | +const sessions: PickerSessionCompressed<any>[] = []; |
| 12 | + |
| 13 | +/** |
| 14 | + * Maximum number of sessions to keep in memory. |
| 15 | + * Oldest sessions are removed when this limit is exceeded. |
| 16 | + */ |
| 17 | +const MAX_SESSION_COUNT = 100; |
| 18 | + |
| 19 | +/** |
| 20 | + * Represents a picker session with all its state information. |
| 21 | + * @template T - The type of item detail in the picker |
| 22 | + */ |
| 23 | +export type PickerSession<T extends Detail> = { |
| 24 | + readonly name: string; |
| 25 | + /** Arguments passed to the source */ |
| 26 | + readonly args: readonly string[]; |
| 27 | + /** The internal state context of the picker */ |
| 28 | + readonly context: PickerContext<T>; |
| 29 | +}; |
| 30 | + |
| 31 | +/** |
| 32 | + * Compressed version of PickerSession where the context is stored as binary data. |
| 33 | + * This reduces memory usage when storing multiple sessions. |
| 34 | + * @template T - The type of item detail in the picker |
| 35 | + */ |
| 36 | +export type PickerSessionCompressed<T extends Detail> = |
| 37 | + & Omit<PickerSession<T>, "context"> |
| 38 | + & { |
| 39 | + /** Brotli-compressed binary representation of the context */ |
| 40 | + context: Uint8Array; |
| 41 | + }; |
| 42 | + |
| 43 | +/** |
| 44 | + * Compresses a picker session by converting its context to brotli-compressed binary data. |
| 45 | + * This is used internally to reduce memory usage when storing sessions. |
| 46 | + * @template T - The type of item detail in the picker |
| 47 | + * @param session - The session to compress |
| 48 | + * @returns A promise that resolves to the compressed session |
| 49 | + */ |
| 50 | +async function compressPickerSession<T extends Detail>( |
| 51 | + session: PickerSession<T>, |
| 52 | +): Promise<PickerSessionCompressed<T>> { |
| 53 | + const encoder = new TextEncoder(); |
| 54 | + return { |
| 55 | + ...session, |
| 56 | + context: await brotli.compress( |
| 57 | + encoder.encode(JSON.stringify(session.context)), |
| 58 | + ), |
| 59 | + }; |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * Decompresses a picker session by converting its binary context back to structured data. |
| 64 | + * @template T - The type of item detail in the picker |
| 65 | + * @param compressed - The compressed session to decompress |
| 66 | + * @returns A promise that resolves to the decompressed session |
| 67 | + */ |
| 68 | +async function decompressPickerSession<T extends Detail>( |
| 69 | + compressed: PickerSessionCompressed<T>, |
| 70 | +): Promise<PickerSession<T>> { |
| 71 | + const decoder = new TextDecoder(); |
| 72 | + return { |
| 73 | + ...compressed, |
| 74 | + context: JSON.parse( |
| 75 | + decoder.decode(await brotli.uncompress(compressed.context)), |
| 76 | + ), |
| 77 | + }; |
| 78 | +} |
| 79 | + |
| 80 | +/** |
| 81 | + * Lists all stored picker sessions in reverse chronological order (newest first). |
| 82 | + * @returns A readonly array of compressed sessions |
| 83 | + */ |
| 84 | +export function listPickerSessions(): readonly PickerSessionCompressed< |
| 85 | + Detail |
| 86 | +>[] { |
| 87 | + return sessions.slice().reverse(); // Return a copy in reverse order |
| 88 | +} |
| 89 | + |
| 90 | +/** |
| 91 | + * Saves a picker session to the in-memory storage. |
| 92 | + * The session is compressed before storage to reduce memory usage. |
| 93 | + * If the storage exceeds MAX_SESSION_COUNT, the oldest session is removed. |
| 94 | + * @template T - The type of item detail in the picker |
| 95 | + * @param session - The session to save |
| 96 | + * @returns A promise that resolves when the session is saved |
| 97 | + */ |
| 98 | +export async function savePickerSession<T extends Detail>( |
| 99 | + session: PickerSession<T>, |
| 100 | +): Promise<void> { |
| 101 | + const compressed = await compressPickerSession(session); |
| 102 | + sessions.push(compressed); |
| 103 | + if (sessions.length > MAX_SESSION_COUNT) { |
| 104 | + sessions.shift(); // Keep only the last MAX_SESSION_COUNT sessions |
| 105 | + } |
| 106 | +} |
| 107 | + |
| 108 | +/** |
| 109 | + * Options for loading a picker session. |
| 110 | + */ |
| 111 | +export type LoadPickerSessionOptions = { |
| 112 | + /** Optional name to filter sessions by source name */ |
| 113 | + name?: string; |
| 114 | + /** Optional number from the latest session to load (1 = most recent, 2 = second most recent, etc.) */ |
| 115 | + number?: number; |
| 116 | +}; |
| 117 | + |
| 118 | +/** |
| 119 | + * Loads a picker session from storage. |
| 120 | + * @template T - The type of item detail in the picker |
| 121 | + * @param indexFromLatest - The index from the latest session (0 = most recent, 1 = second most recent, etc.) |
| 122 | + * @param options - Options to filter sessions |
| 123 | + * @returns A promise that resolves to the decompressed session, or undefined if not found |
| 124 | + * @example |
| 125 | + * ```ts |
| 126 | + * // Load the most recent session |
| 127 | + * const session1 = await loadPickerSession(); |
| 128 | + * |
| 129 | + * // Load the second most recent session |
| 130 | + * const session2 = await loadPickerSession({ number: 2 }); |
| 131 | + * |
| 132 | + * // Load the most recent session with name "file" |
| 133 | + * const session3 = await loadPickerSession({ name: "file", number: 1 }); |
| 134 | + * ``` |
| 135 | + */ |
| 136 | +export async function loadPickerSession<T extends Detail>( |
| 137 | + { name, number: indexFromLatest }: LoadPickerSessionOptions = {}, |
| 138 | +): Promise<PickerSession<T> | undefined> { |
| 139 | + const filteredSessions = name |
| 140 | + ? sessions.filter((s) => s.name === name) |
| 141 | + : sessions; |
| 142 | + const index = filteredSessions.length - (indexFromLatest ?? 1); |
| 143 | + const compressed = filteredSessions.at(index); |
| 144 | + if (!compressed) { |
| 145 | + return undefined; |
| 146 | + } |
| 147 | + return await decompressPickerSession(compressed); |
| 148 | +} |
0 commit comments