dsh-sessions-manager 3.2.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ // Durable "starred sessions" index (schema v3).
2
+ //
3
+ // Deliberately mirrors the recycle-bin index in src/index.js: version field,
4
+ // automatic upgrade of older shapes, atomic write (tmp + rename) and a single
5
+ // chained mutation queue so two concurrent requests can never clobber each
6
+ // other. Extracted from the host bundle so it can be unit-tested directly —
7
+ // pass `dir` to point the index at a temp directory.
8
+ import { mkdir, rename, writeFile } from 'node:fs/promises'
9
+ import { readFileSync } from 'node:fs'
10
+ import { homedir } from 'node:os'
11
+ import { join } from 'node:path'
12
+
13
+ // v3 is the first star schema; it starts at 3 so it can never be confused with
14
+ // the recycle bin's v1/v2 documents even if a file is copied between them.
15
+ export const STAR_SCHEMA_VERSION = 3
16
+
17
+ const DEFAULT_STAR_DIR = join(homedir(), '.dsh', 'sessions-manager')
18
+
19
+ function isSafeSessionId(value) {
20
+ return typeof value === 'string' && value.length > 0 && value.length <= 200 && !/[\\/\0]/.test(value) && value !== '.' && value !== '..'
21
+ }
22
+
23
+ /**
24
+ * Coerce anything on disk (or nothing at all) into a valid v3 store.
25
+ * Accepts a bare array of ids (the pre-schema shape) and upgrades it.
26
+ */
27
+ export function normalizeStarStore(raw) {
28
+ const legacy = Array.isArray(raw) ? raw : null
29
+ const source = legacy || (raw && typeof raw === 'object' ? raw : null)
30
+ const ids = source && Array.isArray(source.starredSessionIds) ? source.starredSessionIds : (legacy || [])
31
+ const clean = []
32
+ const seen = new Set()
33
+ for (const id of ids) {
34
+ // Strings only: silently coercing a number into an id would let junk into
35
+ // the index and mask a caller bug.
36
+ if (!isSafeSessionId(id)) continue
37
+ if (seen.has(id)) continue
38
+ seen.add(id)
39
+ clean.push(id)
40
+ }
41
+ return { schemaVersion: STAR_SCHEMA_VERSION, starredSessionIds: clean }
42
+ }
43
+
44
+ /**
45
+ * Open the star index.
46
+ * @param {object} [options]
47
+ * @param {string} [options.dir] - Directory holding the index (tests inject a temp dir).
48
+ * @param {string} [options.indexPath] - Full index path, overriding `dir`.
49
+ */
50
+ export function createStarIndex(options = {}) {
51
+ const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || DEFAULT_STAR_DIR
52
+ const indexPath = options.indexPath || join(dir, 'star.json')
53
+ let mutation = Promise.resolve()
54
+
55
+ async function read() {
56
+ try {
57
+ return normalizeStarStore(JSON.parse(readFileSync(indexPath, 'utf8')))
58
+ } catch {
59
+ return normalizeStarStore(null)
60
+ }
61
+ }
62
+
63
+ async function write(store) {
64
+ await mkdir(dir, { recursive: true })
65
+ const tmp = join(dir, `.star-${process.pid}-${Date.now()}.tmp`)
66
+ await writeFile(tmp, JSON.stringify(normalizeStarStore(store), null, 2), { encoding: 'utf8', mode: 0o600 })
67
+ await rename(tmp, indexPath)
68
+ }
69
+
70
+ // Serialize read-modify-write cycles: every mutator sees the store as left by
71
+ // the previous one, and a rejected mutator still keeps the chain alive.
72
+ function mutate(mutator) {
73
+ const operation = mutation.then(async () => {
74
+ const store = await read()
75
+ const result = await mutator(store)
76
+ await write(store)
77
+ return result
78
+ })
79
+ mutation = operation.catch(() => {})
80
+ return operation
81
+ }
82
+
83
+ /**
84
+ * Star or unstar sessions.
85
+ * @param {string[]} ids - Session ids to change.
86
+ * @param {boolean} starred - true to star, false to unstar.
87
+ * @returns {Promise<string[]>} The full starred set after the change.
88
+ */
89
+ function setStarred(ids, starred) {
90
+ const wanted = (Array.isArray(ids) ? ids : []).filter(isSafeSessionId).map(String)
91
+ return mutate((store) => {
92
+ const set = new Set(store.starredSessionIds)
93
+ for (const id of wanted) {
94
+ if (starred) set.add(id)
95
+ else set.delete(id)
96
+ }
97
+ store.starredSessionIds = [...set]
98
+ return store.starredSessionIds
99
+ })
100
+ }
101
+
102
+ // Drop ids once their session is gone (purged / deleted), otherwise the index
103
+ // would grow forever with ids that can never be listed again.
104
+ function removeIds(ids) {
105
+ return setStarred(ids, false)
106
+ }
107
+
108
+ return { read, write, mutate, setStarred, removeIds, indexPath, dir }
109
+ }