termcast 1.3.54 → 1.4.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.
Files changed (169) hide show
  1. package/dist/action-utils.d.ts.map +1 -1
  2. package/dist/action-utils.js +17 -132
  3. package/dist/action-utils.js.map +1 -1
  4. package/dist/apis/cache.d.ts +8 -30
  5. package/dist/apis/cache.d.ts.map +1 -1
  6. package/dist/apis/cache.js +9 -271
  7. package/dist/apis/cache.js.map +1 -1
  8. package/dist/apis/clipboard.d.ts +4 -2
  9. package/dist/apis/clipboard.d.ts.map +1 -1
  10. package/dist/apis/clipboard.js +18 -31
  11. package/dist/apis/clipboard.js.map +1 -1
  12. package/dist/apis/environment.d.ts.map +1 -1
  13. package/dist/apis/environment.js +14 -49
  14. package/dist/apis/environment.js.map +1 -1
  15. package/dist/apis/localstorage.d.ts +7 -12
  16. package/dist/apis/localstorage.d.ts.map +1 -1
  17. package/dist/apis/localstorage.js +7 -184
  18. package/dist/apis/localstorage.js.map +1 -1
  19. package/dist/app.d.ts.map +1 -1
  20. package/dist/app.js +16 -15
  21. package/dist/app.js.map +1 -1
  22. package/dist/cli.js +7 -6
  23. package/dist/cli.js.map +1 -1
  24. package/dist/components/actions.d.ts.map +1 -1
  25. package/dist/components/actions.js +13 -2
  26. package/dist/components/actions.js.map +1 -1
  27. package/dist/components/extension-preferences.d.ts.map +1 -1
  28. package/dist/components/extension-preferences.js +7 -8
  29. package/dist/components/extension-preferences.js.map +1 -1
  30. package/dist/components/form/file-autocomplete.js +2 -2
  31. package/dist/components/form/file-autocomplete.js.map +1 -1
  32. package/dist/components/list.d.ts.map +1 -1
  33. package/dist/components/list.js +242 -14
  34. package/dist/components/list.js.map +1 -1
  35. package/dist/e2e-node.d.ts.map +1 -1
  36. package/dist/e2e-node.js +5 -4
  37. package/dist/e2e-node.js.map +1 -1
  38. package/dist/extensions/dev.d.ts.map +1 -1
  39. package/dist/extensions/dev.js +5 -2
  40. package/dist/extensions/dev.js.map +1 -1
  41. package/dist/globals.d.ts.map +1 -1
  42. package/dist/globals.js +2 -1
  43. package/dist/globals.js.map +1 -1
  44. package/dist/internal/error-handler.d.ts.map +1 -1
  45. package/dist/internal/error-handler.js +21 -19
  46. package/dist/internal/error-handler.js.map +1 -1
  47. package/dist/internal/providers.d.ts.map +1 -1
  48. package/dist/internal/providers.js +41 -1
  49. package/dist/internal/providers.js.map +1 -1
  50. package/dist/logger.d.ts.map +1 -1
  51. package/dist/logger.js +31 -29
  52. package/dist/logger.js.map +1 -1
  53. package/dist/platform/browser/cache.d.ts +41 -0
  54. package/dist/platform/browser/cache.d.ts.map +1 -0
  55. package/dist/platform/browser/cache.js +262 -0
  56. package/dist/platform/browser/cache.js.map +1 -0
  57. package/dist/platform/browser/localstorage.d.ts +20 -0
  58. package/dist/platform/browser/localstorage.d.ts.map +1 -0
  59. package/dist/platform/browser/localstorage.js +102 -0
  60. package/dist/platform/browser/localstorage.js.map +1 -0
  61. package/dist/platform/browser/runtime.d.ts +51 -0
  62. package/dist/platform/browser/runtime.d.ts.map +1 -0
  63. package/dist/platform/browser/runtime.js +164 -0
  64. package/dist/platform/browser/runtime.js.map +1 -0
  65. package/dist/platform/bun/sqlite.d.ts +17 -0
  66. package/dist/platform/bun/sqlite.d.ts.map +1 -0
  67. package/dist/platform/bun/sqlite.js +6 -0
  68. package/dist/platform/bun/sqlite.js.map +1 -0
  69. package/dist/platform/node/cache.d.ts +35 -0
  70. package/dist/platform/node/cache.d.ts.map +1 -0
  71. package/dist/platform/node/cache.js +269 -0
  72. package/dist/platform/node/cache.js.map +1 -0
  73. package/dist/platform/node/localstorage.d.ts +17 -0
  74. package/dist/platform/node/localstorage.d.ts.map +1 -0
  75. package/dist/platform/node/localstorage.js +186 -0
  76. package/dist/platform/node/localstorage.js.map +1 -0
  77. package/dist/platform/node/runtime.d.ts +52 -0
  78. package/dist/platform/node/runtime.d.ts.map +1 -0
  79. package/dist/platform/node/runtime.js +230 -0
  80. package/dist/platform/node/runtime.js.map +1 -0
  81. package/dist/platform/node/sqlite.d.ts +27 -0
  82. package/dist/platform/node/sqlite.d.ts.map +1 -0
  83. package/dist/platform/node/sqlite.js +21 -0
  84. package/dist/platform/node/sqlite.js.map +1 -0
  85. package/dist/state.d.ts +5 -0
  86. package/dist/state.d.ts.map +1 -1
  87. package/dist/state.js +6 -28
  88. package/dist/state.js.map +1 -1
  89. package/dist/utils/file-system.d.ts.map +1 -1
  90. package/dist/utils/file-system.js +17 -22
  91. package/dist/utils/file-system.js.map +1 -1
  92. package/dist/utils.d.ts +1 -1
  93. package/dist/utils.d.ts.map +1 -1
  94. package/dist/utils.js +42 -47
  95. package/dist/utils.js.map +1 -1
  96. package/dist/vim-mode.d.ts +40 -0
  97. package/dist/vim-mode.d.ts.map +1 -0
  98. package/dist/vim-mode.js +135 -0
  99. package/dist/vim-mode.js.map +1 -0
  100. package/fonts/Inconsolata.otf +0 -0
  101. package/fonts/SIL Open Font License.txt +41 -0
  102. package/package.json +60 -8
  103. package/src/action-utils.tsx +27 -124
  104. package/src/apis/cache.test.ts +1 -1
  105. package/src/apis/cache.tsx +9 -373
  106. package/src/apis/clipboard.tsx +29 -38
  107. package/src/apis/environment.tsx +25 -52
  108. package/src/apis/localstorage.tsx +8 -214
  109. package/src/app.tsx +16 -15
  110. package/src/cli.tsx +14 -15
  111. package/src/compile.vitest.tsx +2 -2
  112. package/src/components/actions.tsx +19 -1
  113. package/src/components/extension-preferences.tsx +7 -8
  114. package/src/components/form/file-autocomplete.tsx +2 -2
  115. package/src/components/list.tsx +279 -14
  116. package/src/e2e-node.tsx +7 -7
  117. package/src/examples/action-shortcut.vitest.tsx +2 -2
  118. package/src/examples/actions-context.vitest.tsx +1 -1
  119. package/src/examples/bar-graph-weekly.vitest.tsx +10 -36
  120. package/src/examples/detail-metadata-showcase.vitest.tsx +36 -36
  121. package/src/examples/form-basic.vitest.tsx +21 -17
  122. package/src/examples/github.vitest.tsx +4 -4
  123. package/src/examples/graph-bar-chart.vitest.tsx +13 -11
  124. package/src/examples/graph-polymarket.vitest.tsx +2 -2
  125. package/src/examples/graph-row.vitest.tsx +66 -66
  126. package/src/examples/graph-styles.vitest.tsx +12 -12
  127. package/src/examples/internal/simple-scrollbox.vitest.tsx +14 -48
  128. package/src/examples/list-detail-metadata.vitest.tsx +5 -5
  129. package/src/examples/list-fetch-data.vitest.tsx +3 -3
  130. package/src/examples/list-item-accessories.vitest.tsx +2 -2
  131. package/src/examples/list-loading-empty-view.vitest.tsx +1 -1
  132. package/src/examples/list-no-actions.vitest.tsx +2 -2
  133. package/src/examples/list-scrollbox.vitest.tsx +5 -5
  134. package/src/examples/list-spacing-mode.vitest.tsx +3 -3
  135. package/src/examples/list-with-detail.vitest.tsx +68 -68
  136. package/src/examples/list-with-dropdown.vitest.tsx +5 -5
  137. package/src/examples/list-with-sections.vitest.tsx +27 -27
  138. package/src/examples/simple-candle-chart.vitest.tsx +7 -7
  139. package/src/examples/simple-detail-markdown.vitest.tsx +8 -8
  140. package/src/examples/simple-detail-table.vitest.tsx +8 -8
  141. package/src/examples/simple-graph.vitest.tsx +3 -3
  142. package/src/examples/simple-grid.vitest.tsx +14 -14
  143. package/src/examples/simple-heatmap.vitest.tsx +1 -1
  144. package/src/examples/simple-navigation.vitest.tsx +17 -17
  145. package/src/examples/simple-progress-bar.vitest.tsx +1 -1
  146. package/src/examples/store.vitest.tsx +1 -1
  147. package/src/examples/swift-extension.vitest.tsx +2 -2
  148. package/src/examples/table-edge-cases.vitest.tsx +18 -18
  149. package/src/examples/toast-action.vitest.tsx +2 -2
  150. package/src/extensions/dev.tsx +5 -2
  151. package/src/extensions/dev.vitest.tsx +3 -3
  152. package/src/globals.ts +2 -1
  153. package/src/internal/error-handler.tsx +19 -21
  154. package/src/internal/providers.tsx +39 -0
  155. package/src/logger.tsx +38 -41
  156. package/src/platform/browser/cache.ts +327 -0
  157. package/src/platform/browser/localstorage.ts +119 -0
  158. package/src/platform/browser/runtime.ts +209 -0
  159. package/src/platform/bun/sqlite.ts +19 -0
  160. package/src/platform/node/cache.ts +372 -0
  161. package/src/platform/node/localstorage.ts +214 -0
  162. package/src/platform/node/runtime.ts +264 -0
  163. package/src/platform/node/sqlite.ts +43 -0
  164. package/src/state.tsx +17 -28
  165. package/src/utils/file-system.ts +17 -22
  166. package/src/utils.test.tsx +1 -1
  167. package/src/utils.tsx +56 -47
  168. package/src/vim-mode.tsx +153 -0
  169. package/src/apis/sqlite.ts +0 -14
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Node/Bun Cache implementation backed by SQLite.
3
+ * Moved from apis/cache.tsx — this is the platform-specific storage layer.
4
+ */
5
+
6
+ import { Database, type DatabaseInstance } from '#sqlite'
7
+ import { joinPath, dirname, homedir, ensureDir, byteLength } from '#platform/runtime'
8
+ import { logger } from '../../logger'
9
+ import { useStore } from '../../state'
10
+
11
+ const CACHE_TABLE_NAME = 'cache_entries'
12
+ const DEFAULT_NAMESPACE = '__default__'
13
+ const initializedDatabasePaths = new Set<string>()
14
+ let logicalTimestamp = Date.now()
15
+
16
+ function nextTimestamp(): number {
17
+ logicalTimestamp += 1
18
+ return logicalTimestamp
19
+ }
20
+
21
+ function getCurrentDatabasePath(): string {
22
+ const { extensionPath } = useStore.getState()
23
+ const dbSuffix = (globalThis as any).process?.env?.TERMCAST_DB_SUFFIX?.replace(/[^a-zA-Z0-9_-]/g, '_')
24
+ const dbFileName = dbSuffix ? `data-${dbSuffix}.db` : 'data.db'
25
+
26
+ if (extensionPath) {
27
+ return joinPath(extensionPath, '.termcast-bundle', dbFileName)
28
+ }
29
+
30
+ // Fallback for examples/tests that don't set extensionPath
31
+ return joinPath(homedir(), '.termcast', '.termcast-bundle', dbFileName)
32
+ }
33
+
34
+ function getCurrentCacheDir(namespace?: string): string {
35
+ const { extensionPath } = useStore.getState()
36
+
37
+ const baseDir = extensionPath
38
+ ? joinPath(extensionPath, '.termcast-bundle', 'cache')
39
+ : joinPath(homedir(), '.termcast', '.termcast-bundle', 'cache')
40
+
41
+ return namespace ? joinPath(baseDir, namespace) : baseDir
42
+ }
43
+
44
+ function getNamespace(namespace?: string): string {
45
+ return namespace || DEFAULT_NAMESPACE
46
+ }
47
+
48
+ function initializeDatabaseOnce({ db, dbPath }: { db: DatabaseInstance; dbPath: string }): void {
49
+ if (initializedDatabasePaths.has(dbPath)) {
50
+ return
51
+ }
52
+
53
+ db.exec('PRAGMA journal_mode = WAL')
54
+ db.exec('PRAGMA wal_autocheckpoint = 1000')
55
+ db.exec('PRAGMA synchronous = NORMAL')
56
+
57
+ db.exec(`
58
+ CREATE TABLE IF NOT EXISTS ${CACHE_TABLE_NAME} (
59
+ namespace TEXT NOT NULL,
60
+ key TEXT NOT NULL,
61
+ data TEXT NOT NULL,
62
+ size INTEGER NOT NULL,
63
+ last_accessed_at INTEGER NOT NULL,
64
+ updated_at INTEGER NOT NULL,
65
+ PRIMARY KEY(namespace, key)
66
+ )
67
+ `)
68
+
69
+ db.exec(`
70
+ CREATE INDEX IF NOT EXISTS idx_${CACHE_TABLE_NAME}_namespace_lru
71
+ ON ${CACHE_TABLE_NAME}(namespace, last_accessed_at)
72
+ `)
73
+
74
+ cleanupLegacyCacheTables(db)
75
+ initializedDatabasePaths.add(dbPath)
76
+ }
77
+
78
+ function cleanupLegacyCacheTables(db: DatabaseInstance): void {
79
+ const rows = db
80
+ .prepare(
81
+ `SELECT name FROM sqlite_master
82
+ WHERE type = 'table'
83
+ AND (name = 'cache' OR name LIKE 'cache_%')
84
+ AND name != ?`,
85
+ )
86
+ .all(CACHE_TABLE_NAME) as Array<{ name: string }>
87
+
88
+ if (rows.length === 0) {
89
+ return
90
+ }
91
+
92
+ const tx = db.transaction(() => {
93
+ const upsert = db.prepare(
94
+ `INSERT INTO ${CACHE_TABLE_NAME} (namespace, key, data, size, last_accessed_at, updated_at)
95
+ VALUES (?, ?, ?, ?, ?, ?)
96
+ ON CONFLICT(namespace, key)
97
+ DO UPDATE SET
98
+ data = excluded.data,
99
+ size = excluded.size,
100
+ last_accessed_at = excluded.last_accessed_at,
101
+ updated_at = excluded.updated_at`,
102
+ )
103
+
104
+ for (const { name } of rows) {
105
+ const namespace =
106
+ name === 'cache'
107
+ ? DEFAULT_NAMESPACE
108
+ : name === 'cache_tanstack_query'
109
+ ? 'tanstack-query'
110
+ : `legacy:${name.slice('cache_'.length)}`
111
+
112
+ try {
113
+ const values = db
114
+ .prepare(`SELECT key, data, size, rowid FROM ${name}`)
115
+ .all() as Array<{ key: string; data: string; size: number; rowid: number }>
116
+
117
+ values.forEach((entry) => {
118
+ const timestamp = entry.rowid
119
+ upsert.run(
120
+ namespace,
121
+ entry.key,
122
+ entry.data,
123
+ entry.size,
124
+ timestamp,
125
+ timestamp,
126
+ )
127
+ })
128
+ } catch {
129
+ // Ignore invalid legacy tables and continue cleanup.
130
+ }
131
+
132
+ db.exec(`DROP TABLE IF EXISTS ${name}`)
133
+ }
134
+ })
135
+
136
+ tx()
137
+ }
138
+
139
+ export class Cache {
140
+ static get STORAGE_DIRECTORY_NAME(): string {
141
+ const extensionPath = useStore.getState().extensionPath
142
+ return extensionPath ? 'cache' : '.termcast-cache'
143
+ }
144
+
145
+ static get DEFAULT_CAPACITY(): number {
146
+ return 10 * 1024 * 1024 // 10 MB
147
+ }
148
+
149
+ private db: DatabaseInstance
150
+ private capacity: number
151
+ private namespace: string
152
+ private subscribers: Cache.Subscriber[] = []
153
+ private currentSize: number = 0
154
+
155
+ constructor(options?: Cache.Options) {
156
+ const sqliteLoadStart = Date.now()
157
+ this.capacity = options?.capacity || Cache.DEFAULT_CAPACITY
158
+ this.namespace = getNamespace(options?.namespace)
159
+
160
+ const dbPath = getCurrentDatabasePath()
161
+
162
+ // Ensure parent directory exists
163
+ const dbDir = dirname(dbPath)
164
+ ensureDir(dbDir)
165
+
166
+ this.db = new Database(dbPath, {
167
+ create: true,
168
+ readwrite: true,
169
+ })
170
+
171
+ initializeDatabaseOnce({ db: this.db, dbPath })
172
+
173
+ // Calculate initial size
174
+ const row = this.db
175
+ .prepare(
176
+ `SELECT COALESCE(SUM(size), 0) as total FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`,
177
+ )
178
+ .get(this.namespace) as { total: number | null } | undefined
179
+ this.currentSize = row?.total || 0
180
+
181
+ const sqliteLoadMs = Date.now() - sqliteLoadStart
182
+ if (sqliteLoadMs > 500) {
183
+ logger.log(
184
+ `[perf] sqlite cache init took ${sqliteLoadMs}ms (namespace=${this.namespace})`,
185
+ )
186
+ }
187
+
188
+ // Bind all methods to this instance
189
+ this.get = this.get.bind(this)
190
+ this.has = this.has.bind(this)
191
+ this.set = this.set.bind(this)
192
+ this.remove = this.remove.bind(this)
193
+ this.clear = this.clear.bind(this)
194
+ this.subscribe = this.subscribe.bind(this)
195
+ this.maintainCapacity = this.maintainCapacity.bind(this)
196
+ this.notifySubscribers = this.notifySubscribers.bind(this)
197
+ }
198
+
199
+ get storageDirectory(): string {
200
+ return getCurrentCacheDir(this.namespace)
201
+ }
202
+
203
+ get(key: string): string | undefined {
204
+ const now = nextTimestamp()
205
+ const row = this.db
206
+ .prepare(
207
+ `SELECT data, size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
208
+ )
209
+ .get(this.namespace, key) as { data: string; size: number } | undefined
210
+
211
+ if (row) {
212
+ this.db
213
+ .prepare(
214
+ `UPDATE ${CACHE_TABLE_NAME} SET last_accessed_at = ? WHERE namespace = ? AND key = ?`,
215
+ )
216
+ .run(now, this.namespace, key)
217
+
218
+ return row.data
219
+ }
220
+
221
+ return undefined
222
+ }
223
+
224
+ has(key: string): boolean {
225
+ const row = this.db
226
+ .prepare(`SELECT 1 FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`)
227
+ .get(this.namespace, key)
228
+ return !!row
229
+ }
230
+
231
+ get isEmpty(): boolean {
232
+ const row = this.db
233
+ .prepare(
234
+ `SELECT COUNT(*) as count FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`,
235
+ )
236
+ .get(this.namespace) as { count: number }
237
+ return row.count === 0
238
+ }
239
+
240
+ set(key: string, data: string): void {
241
+ const now = nextTimestamp()
242
+ const dataSize = byteLength(data)
243
+
244
+ // Get existing size if any
245
+ const existingRow = this.db
246
+ .prepare(
247
+ `SELECT size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
248
+ )
249
+ .get(this.namespace, key) as { size: number } | undefined
250
+ const oldSize = existingRow?.size || 0
251
+ const newTotalSize = this.currentSize - oldSize + dataSize
252
+
253
+ if (newTotalSize > this.capacity) {
254
+ this.maintainCapacity(newTotalSize - this.capacity)
255
+ }
256
+
257
+ // Insert or update the cache entry
258
+ this.db
259
+ .prepare(
260
+ `INSERT INTO ${CACHE_TABLE_NAME} (namespace, key, data, size, last_accessed_at, updated_at)
261
+ VALUES (?, ?, ?, ?, ?, ?)
262
+ ON CONFLICT(namespace, key)
263
+ DO UPDATE SET
264
+ data = excluded.data,
265
+ size = excluded.size,
266
+ last_accessed_at = excluded.last_accessed_at,
267
+ updated_at = excluded.updated_at`,
268
+ )
269
+ .run(this.namespace, key, data, dataSize, now, now)
270
+
271
+ this.currentSize = this.currentSize - oldSize + dataSize
272
+ this.notifySubscribers(key, data)
273
+ }
274
+
275
+ remove(key: string): boolean {
276
+ const row = this.db
277
+ .prepare(
278
+ `SELECT size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
279
+ )
280
+ .get(this.namespace, key) as { size: number } | undefined
281
+
282
+ if (row) {
283
+ this.db
284
+ .prepare(`DELETE FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`)
285
+ .run(this.namespace, key)
286
+
287
+ this.currentSize -= row.size
288
+ this.notifySubscribers(key, undefined)
289
+ return true
290
+ }
291
+
292
+ return false
293
+ }
294
+
295
+ clear(options?: { notifySubscribers: boolean }): void {
296
+ this.db
297
+ .prepare(`DELETE FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`)
298
+ .run(this.namespace)
299
+ this.currentSize = 0
300
+
301
+ if (options?.notifySubscribers !== false) {
302
+ this.notifySubscribers(undefined, undefined)
303
+ }
304
+ }
305
+
306
+ subscribe(subscriber: Cache.Subscriber): Cache.Subscription {
307
+ this.subscribers.push(subscriber)
308
+ return () => {
309
+ const index = this.subscribers.indexOf(subscriber)
310
+ if (index > -1) {
311
+ this.subscribers.splice(index, 1)
312
+ }
313
+ }
314
+ }
315
+
316
+ private maintainCapacity(bytesToFree: number): void {
317
+ const rows = this.db
318
+ .prepare(
319
+ `SELECT key, size FROM ${CACHE_TABLE_NAME}
320
+ WHERE namespace = ?
321
+ ORDER BY last_accessed_at ASC`,
322
+ )
323
+ .all(this.namespace) as Array<{ key: string; size: number }>
324
+
325
+ let freedBytes = 0
326
+ const keysToRemove: string[] = []
327
+
328
+ for (const row of rows) {
329
+ if (freedBytes >= bytesToFree) {
330
+ break
331
+ }
332
+ keysToRemove.push(row.key)
333
+ freedBytes += row.size
334
+ }
335
+
336
+ if (keysToRemove.length > 0) {
337
+ const placeholders = keysToRemove.map(() => '?').join(',')
338
+ const stmt = this.db.prepare(
339
+ `DELETE FROM ${CACHE_TABLE_NAME}
340
+ WHERE namespace = ? AND key IN (${placeholders})`,
341
+ )
342
+ stmt.run(this.namespace, ...(keysToRemove as [string, ...string[]]))
343
+ this.currentSize -= freedBytes
344
+ }
345
+ }
346
+
347
+ private notifySubscribers(
348
+ key: string | undefined,
349
+ data: string | undefined,
350
+ ): void {
351
+ for (const subscriber of this.subscribers) {
352
+ try {
353
+ subscriber(key, data)
354
+ } catch (error) {
355
+ logger.error('Cache subscriber error:', error)
356
+ }
357
+ }
358
+ }
359
+ }
360
+
361
+ export namespace Cache {
362
+ export interface Options {
363
+ namespace?: string
364
+ capacity?: number
365
+ }
366
+
367
+ export type Subscriber = (
368
+ key: string | undefined,
369
+ data: string | undefined,
370
+ ) => void
371
+ export type Subscription = () => void
372
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Node/Bun LocalStorage implementation backed by SQLite.
3
+ * Moved from apis/localstorage.tsx.
4
+ */
5
+
6
+ import { Database, type DatabaseInstance } from '#sqlite'
7
+ import { joinPath, dirname, ensureDir } from '#platform/runtime'
8
+ import { logger } from '../../logger'
9
+ import { useStore } from '../../state'
10
+
11
+ let db: DatabaseInstance | null = null
12
+ let currentDbPath: string | null = null
13
+
14
+ function getCurrentDatabasePath(): string {
15
+ const { extensionPath } = useStore.getState()
16
+
17
+ if (!extensionPath) {
18
+ throw new Error('Cannot access LocalStorage - extensionPath not set')
19
+ }
20
+
21
+ return joinPath(extensionPath, '.termcast-bundle', 'data.db')
22
+ }
23
+
24
+ function getDatabase(): DatabaseInstance {
25
+ const dbPath = getCurrentDatabasePath()
26
+
27
+ // Check if we need to reconnect due to path change
28
+ if (db && currentDbPath !== dbPath) {
29
+ db.close()
30
+ db = null
31
+ currentDbPath = null
32
+ }
33
+
34
+ if (!db) {
35
+ // Ensure parent directory exists
36
+ const dbDir = dirname(dbPath)
37
+ ensureDir(dbDir)
38
+
39
+ db = new Database(dbPath, {
40
+ create: true,
41
+ readwrite: true,
42
+ })
43
+ currentDbPath = dbPath
44
+
45
+ // Use WAL mode and optimize for single file usage
46
+ db.exec('PRAGMA journal_mode = WAL')
47
+ db.exec('PRAGMA wal_autocheckpoint = 1000')
48
+ db.exec('PRAGMA synchronous = NORMAL')
49
+
50
+ db.exec(`
51
+ CREATE TABLE IF NOT EXISTS localstorage (
52
+ key TEXT PRIMARY KEY,
53
+ value TEXT NOT NULL,
54
+ type TEXT NOT NULL
55
+ )
56
+ `)
57
+ }
58
+ return db
59
+ }
60
+
61
+ export namespace LocalStorage {
62
+ export type Value = string | number | boolean
63
+
64
+ export interface Values {
65
+ [key: string]: any
66
+ }
67
+
68
+ export async function getItem<T extends Value = Value>(
69
+ key: string,
70
+ ): Promise<T | undefined> {
71
+ return new Promise((resolve) => {
72
+ try {
73
+ const db = getDatabase()
74
+ const row = db
75
+ .prepare('SELECT value, type FROM localstorage WHERE key = ?')
76
+ .get(key) as { value: string; type: string } | undefined
77
+
78
+ if (!row) {
79
+ resolve(undefined)
80
+ return
81
+ }
82
+
83
+ let value: Value
84
+ switch (row.type) {
85
+ case 'number':
86
+ value = parseFloat(row.value)
87
+ break
88
+ case 'boolean':
89
+ value = row.value === 'true'
90
+ break
91
+ default:
92
+ value = row.value
93
+ }
94
+
95
+ resolve(value as T)
96
+ } catch (err) {
97
+ logger.error('LocalStorage.getItem error:', err)
98
+ resolve(undefined)
99
+ }
100
+ })
101
+ }
102
+
103
+ export function getItemSync<T extends Value = Value>(
104
+ key: string,
105
+ ): T | undefined {
106
+ try {
107
+ const db = getDatabase()
108
+ const row = db
109
+ .prepare('SELECT value, type FROM localstorage WHERE key = ?')
110
+ .get(key) as { value: string; type: string } | undefined
111
+
112
+ if (!row) {
113
+ return undefined
114
+ }
115
+
116
+ let value: Value
117
+ switch (row.type) {
118
+ case 'number':
119
+ value = parseFloat(row.value)
120
+ break
121
+ case 'boolean':
122
+ value = row.value === 'true'
123
+ break
124
+ default:
125
+ value = row.value
126
+ }
127
+
128
+ return value as T
129
+ } catch (err) {
130
+ logger.error('LocalStorage.getItemSync error:', err)
131
+ return undefined
132
+ }
133
+ }
134
+
135
+ export async function setItem(key: string, value: Value): Promise<void> {
136
+ return new Promise((resolve, reject) => {
137
+ try {
138
+ const db = getDatabase()
139
+ const type = typeof value
140
+ const stringValue = String(value)
141
+
142
+ db.prepare(
143
+ 'INSERT OR REPLACE INTO localstorage (key, value, type) VALUES (?, ?, ?)',
144
+ ).run(key, stringValue, type)
145
+ resolve()
146
+ } catch (err) {
147
+ logger.error('LocalStorage.setItem error:', err)
148
+ reject(err)
149
+ }
150
+ })
151
+ }
152
+
153
+ export async function removeItem(key: string): Promise<void> {
154
+ return new Promise((resolve, reject) => {
155
+ try {
156
+ const db = getDatabase()
157
+ db.prepare('DELETE FROM localstorage WHERE key = ?').run(key)
158
+ resolve()
159
+ } catch (err) {
160
+ logger.error('LocalStorage.removeItem error:', err)
161
+ reject(err)
162
+ }
163
+ })
164
+ }
165
+
166
+ export async function allItems<T extends Values = Values>(): Promise<T> {
167
+ return new Promise((resolve, reject) => {
168
+ try {
169
+ const db = getDatabase()
170
+ const rows = db
171
+ .prepare('SELECT key, value, type FROM localstorage')
172
+ .all() as Array<{
173
+ key: string
174
+ value: string
175
+ type: string
176
+ }>
177
+
178
+ const result: Values = {}
179
+ for (const row of rows) {
180
+ let value: Value
181
+ switch (row.type) {
182
+ case 'number':
183
+ value = parseFloat(row.value)
184
+ break
185
+ case 'boolean':
186
+ value = row.value === 'true'
187
+ break
188
+ default:
189
+ value = row.value
190
+ }
191
+ result[row.key] = value
192
+ }
193
+
194
+ resolve(result as T)
195
+ } catch (err) {
196
+ logger.error('LocalStorage.allItems error:', err)
197
+ reject(err)
198
+ }
199
+ })
200
+ }
201
+
202
+ export async function clear(): Promise<void> {
203
+ return new Promise((resolve, reject) => {
204
+ try {
205
+ const db = getDatabase()
206
+ db.exec('DELETE FROM localstorage')
207
+ resolve()
208
+ } catch (err) {
209
+ logger.error('LocalStorage.clear error:', err)
210
+ reject(err)
211
+ }
212
+ })
213
+ }
214
+ }