Skip to content

Commit 159a5d1

Browse files
committed
Add session.ts to manage picker sessions in memory
1 parent 33835a2 commit 159a5d1

2 files changed

Lines changed: 445 additions & 0 deletions

File tree

denops/fall/session.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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

Comments
 (0)