opencode-extended-sidebar 0.2.90 → 0.2.91

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 (108) hide show
  1. package/.oesignore +17 -17
  2. package/CHANGELOG.md +96 -95
  3. package/LICENSE +21 -21
  4. package/README.md +150 -235
  5. package/oes.json +13 -11
  6. package/package.json +100 -100
  7. package/scripts/install-git-hooks.mjs +29 -29
  8. package/src/pware.oc.core/constants/index.ts +18 -18
  9. package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -29
  10. package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +15 -13
  11. package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -241
  12. package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -55
  13. package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -65
  14. package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -58
  15. package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -63
  16. package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -34
  17. package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -74
  18. package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -69
  19. package/src/pware.oc.core/git/index.ts +7 -7
  20. package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -237
  21. package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -87
  22. package/src/pware.oc.core/index.ts +19 -18
  23. package/src/pware.oc.core/pware.oc.core.bus.ts +42 -42
  24. package/src/pware.oc.core/pware.oc.core.cache.ts +48 -48
  25. package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -97
  26. package/src/pware.oc.core/pware.oc.core.debug.ts +280 -280
  27. package/src/pware.oc.core/pware.oc.core.events.ts +79 -79
  28. package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -135
  29. package/src/pware.oc.core/pware.oc.core.layout.ts +234 -234
  30. package/src/pware.oc.core/pware.oc.core.oes.ts +99 -89
  31. package/src/pware.oc.core/pware.oc.core.paths.ts +240 -240
  32. package/src/pware.oc.core/pware.oc.core.preview.ts +74 -74
  33. package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -726
  34. package/src/pware.oc.core/pware.oc.core.runtimeConfig.ts +41 -0
  35. package/src/pware.oc.core/pware.oc.core.sqlite.ts +164 -175
  36. package/src/pware.oc.core/pware.oc.core.sqliteGateway.ts +89 -0
  37. package/src/pware.oc.core/pware.oc.core.status.ts +214 -214
  38. package/src/pware.oc.core/pware.oc.core.timing.ts +63 -63
  39. package/src/pware.oc.core/pware.oc.core.width.ts +215 -215
  40. package/src/pware.oc.omo/constants/index.ts +15 -15
  41. package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -18
  42. package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -49
  43. package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -29
  44. package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -11
  45. package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -60
  46. package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -49
  47. package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -25
  48. package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -38
  49. package/src/pware.oc.omo/index.ts +7 -7
  50. package/src/pware.oc.omo/resolver/index.ts +19 -19
  51. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -179
  52. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -116
  53. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -34
  54. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -503
  55. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -40
  56. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -269
  57. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -18
  58. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -18
  59. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -155
  60. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +363 -361
  61. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -18
  62. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -11
  63. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -14
  64. package/src/pware.oc.opencode/constants/index.ts +10 -10
  65. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -15
  66. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -18
  67. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -45
  68. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -58
  69. package/src/pware.oc.opencode/index.ts +8 -8
  70. package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -182
  71. package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -399
  72. package/src/pware.oc.opencode/resolver/index.ts +223 -170
  73. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +150 -162
  74. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +149 -185
  75. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +248 -211
  76. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +22 -26
  77. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +224 -236
  78. package/src/pware.oc.perf/index.ts +14 -14
  79. package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -15
  80. package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -284
  81. package/src/pware.oc.perf/pware.oc.perf.reader.ts +962 -927
  82. package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -88
  83. package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -118
  84. package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -79
  85. package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -71
  86. package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -214
  87. package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -179
  88. package/src/pware.oc.perf/pware.oc.perf.view.tsx +463 -462
  89. package/src/pware.oc.runtime/index.ts +13 -13
  90. package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -142
  91. package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +87 -79
  92. package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +282 -253
  93. package/src/pware.oc.runtime/pware.oc.runtime.omoRead.ts +37 -0
  94. package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +83 -95
  95. package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +130 -129
  96. package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -105
  97. package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +44 -43
  98. package/src/pware.oc.runtime/resolver/index.ts +268 -224
  99. package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -146
  100. package/src/pware.oc.ui/index.ts +12 -12
  101. package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +381 -339
  102. package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -144
  103. package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -111
  104. package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -82
  105. package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -843
  106. package/src/pware.oc.ui/pware.oc.ui.sections.tsx +552 -526
  107. package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1718 -1629
  108. package/src/pware.oc.ui.tsx +98 -98
@@ -0,0 +1,41 @@
1
+ /**
2
+ * pware.oc.core.runtimeConfig
3
+ *
4
+ * The plugin's kv-persisted runtime config as one DTO. Pure — parse and mutate
5
+ * here; the ui layer reads and writes it through the host `api.kv` (a JSON-safe
6
+ * value: plain objects/arrays/strings/numbers/booleans/null only, no Map/Set/
7
+ * Date/class instances).
8
+ */
9
+
10
+ /** Runtime config persisted in the host kv store under `oes.config`. Extend as features land. */
11
+ export type RuntimeConfigDto = {
12
+ /** Session ids pinned to the top of the My work queue, most recently pinned first. */
13
+ pinned_sessions: string[]
14
+ }
15
+
16
+ export function emptyRuntimeConfig(): RuntimeConfigDto {
17
+ return { pinned_sessions: [] }
18
+ }
19
+
20
+ /** Parse any kv value into a DTO, tolerating missing, malformed, or partial input. */
21
+ export function parseRuntimeConfig(raw: unknown): RuntimeConfigDto {
22
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return emptyRuntimeConfig()
23
+ const o = raw as Record<string, unknown>
24
+ const pinned = Array.isArray(o.pinned_sessions)
25
+ ? o.pinned_sessions.filter((id): id is string => typeof id === "string" && id.length > 0)
26
+ : []
27
+ return { pinned_sessions: pinned }
28
+ }
29
+
30
+ /** Pin a session — idempotent; a newly pinned session moves to the front. */
31
+ export function pinSession(config: RuntimeConfigDto, sessionId: string): RuntimeConfigDto {
32
+ if (!sessionId) return config
33
+ const pinned = config.pinned_sessions.filter((id) => id !== sessionId)
34
+ return { ...config, pinned_sessions: [sessionId, ...pinned] }
35
+ }
36
+
37
+ /** Unpin a session — idempotent. */
38
+ export function unpinSession(config: RuntimeConfigDto, sessionId: string): RuntimeConfigDto {
39
+ if (!sessionId) return config
40
+ return { ...config, pinned_sessions: config.pinned_sessions.filter((id) => id !== sessionId) }
41
+ }
@@ -1,175 +1,164 @@
1
- /**
2
- * Thin readonly SQLite handle: prefers bun:sqlite, falls back to node:sqlite.
3
- * Never writes. Returns null when neither backend / DB is available.
4
- */
5
- import { createRequire } from "node:module"
6
- import fs from "node:fs"
7
- import { dbg, profile, pushScreenLine } from "./pware.oc.core.debug.js"
8
-
9
- export type SqlRow = Record<string, unknown>
10
-
11
- export type SqlDb = {
12
- all: <T extends SqlRow = SqlRow>(sql: string, ...params: unknown[]) => T[]
13
- get: <T extends SqlRow = SqlRow>(sql: string, ...params: unknown[]) => T | null
14
- close: () => void
15
- }
16
-
17
- const require = createRequire(import.meta.url)
18
-
19
- /**
20
- * Lock wait before a query fails with SQLITE_BUSY. Fail-fast: a transient WAL
21
- * checkpoint lock should surface as an error row (and a `sql.busy` debug line)
22
- * instead of blocking the whole TUI for seconds.
23
- */
24
- const BUSY_TIMEOUT_MS = 100
25
-
26
- /** One-line query label for the profile log; computed lazily (profiling off = free). */
27
- function sqlLabel(sql: string): string {
28
- return sql.replace(/\s+/g, " ").trim().slice(0, 80)
29
- }
30
-
31
- /** True when a thrown error is a SQLite lock/busy failure (SQLITE_BUSY). */
32
- export function isBusyError(e: unknown): boolean {
33
- return e instanceof Error ? /locked|busy/i.test(e.message) : false
34
- }
35
-
36
- /**
37
- * Run a query, logging a `sql.busy` debug line when the DB is locked. The
38
- * error still propagates so `withDbRead` can retry once and then soft-fail.
39
- */
40
- function guardQuery<T>(sql: string, fn: () => T): T {
41
- try {
42
- return fn()
43
- } catch (e) {
44
- if (isBusyError(e)) dbg("sql.busy", "db locked", { q: sqlLabel(sql) })
45
- throw e
46
- }
47
- }
48
-
49
- function wrapBun(db: {
50
- query: (sql: string) => { all: (...p: unknown[]) => SqlRow[]; get: (...p: unknown[]) => SqlRow | null }
51
- exec: (sql: string) => void
52
- close: () => void
53
- }): SqlDb {
54
- try {
55
- db.exec("PRAGMA query_only = ON")
56
- db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`)
57
- } catch {
58
- // older / restricted
59
- }
60
- return {
61
- all: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
62
- profile("sql", () => guardQuery(sql, () => db.query(sql).all(...params) as T[]), () => ({ q: sqlLabel(sql) })),
63
- get: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
64
- profile("sql", () => guardQuery(sql, () => (db.query(sql).get(...params) as T | null) ?? null), () => ({ q: sqlLabel(sql) })),
65
- close: () => {
66
- try {
67
- db.close()
68
- } catch {
69
- // ignore
70
- }
71
- },
72
- }
73
- }
74
-
75
- function wrapNodeSync(db: {
76
- prepare: (sql: string) => {
77
- all: (...p: unknown[]) => SqlRow[]
78
- get: (...p: unknown[]) => SqlRow | undefined
79
- }
80
- exec: (sql: string) => void
81
- close: () => void
82
- }): SqlDb {
83
- try {
84
- db.exec("PRAGMA query_only = ON")
85
- db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`)
86
- } catch {
87
- // ignore
88
- }
89
- return {
90
- all: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
91
- profile("sql", () => guardQuery(sql, () => db.prepare(sql).all(...params) as T[]), () => ({ q: sqlLabel(sql) })),
92
- get: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
93
- profile("sql", () => guardQuery(sql, () => (db.prepare(sql).get(...params) as T | undefined) ?? null), () => ({ q: sqlLabel(sql) })),
94
- close: () => {
95
- try {
96
- db.close()
97
- } catch {
98
- // ignore
99
- }
100
- },
101
- }
102
- }
103
-
104
- let hold: { path: string; db: SqlDb } | null = null
105
-
106
- function openFresh(dbPath: string): SqlDb | null {
107
- if (!dbPath || !fs.existsSync(dbPath)) return null
108
- try {
109
- if (typeof process.versions.bun === "string") {
110
- const { Database } = require("bun:sqlite") as {
111
- Database: new (
112
- path: string,
113
- opts?: { readonly?: boolean; create?: boolean },
114
- ) => Parameters<typeof wrapBun>[0]
115
- }
116
- return wrapBun(new Database(dbPath, { readonly: true, create: false }))
117
- }
118
- } catch {
119
- // try node next
120
- }
121
- try {
122
- const mod = require("node:sqlite") as {
123
- DatabaseSync: new (
124
- path: string,
125
- opts?: { readOnly?: boolean },
126
- ) => Parameters<typeof wrapNodeSync>[0]
127
- }
128
- if (!mod?.DatabaseSync) return null
129
- return wrapNodeSync(new mod.DatabaseSync(dbPath, { readOnly: true }))
130
- } catch {
131
- return null
132
- }
133
- }
134
-
135
- /** De-duplicated, non-empty string ids for `IN (...)` queries. */
136
- export function uniqueIds(ids: readonly string[]): string[] {
137
- return [...new Set(ids.filter((id) => typeof id === "string" && id.length > 0))]
138
- }
139
-
140
- /** Drop the cached handle so the next open is a real reopen. */
141
- export function resetReadonlyDb(): void {
142
- if (!hold) return
143
- try {
144
- hold.db.close()
145
- } catch {
146
- // ignore
147
- }
148
- hold = null
149
- }
150
-
151
- /** Retry a read after dropping a stale handle. */
152
- export function withDbRead<T>(run: () => T, fallback: (err: unknown) => T): T {
153
- try {
154
- return run()
155
- } catch (e) {
156
- resetReadonlyDb()
157
- try {
158
- return run()
159
- } catch {
160
- return fallback(e)
161
- }
162
- }
163
- }
164
-
165
- /** Open opencode.db readonly. Reuses one handle per path. Never throws — returns null on failure. */
166
- export function openReadonlyDb(dbPath: string): SqlDb | null {
167
- if (!dbPath) return null
168
- if (hold?.path === dbPath) return hold.db
169
- resetReadonlyDb()
170
- const db = openFresh(dbPath)
171
- if (!db) return null
172
- hold = { path: dbPath, db }
173
- pushScreenLine("db open")
174
- return db
175
- }
1
+ /**
2
+ * Thin readonly SQLite handle: prefers bun:sqlite, falls back to node:sqlite.
3
+ * Never writes. Returns null when neither backend / DB is available.
4
+ */
5
+ import { createRequire } from "node:module"
6
+ import fs from "node:fs"
7
+ import { dbg, profile, pushScreenLine } from "./pware.oc.core.debug.js"
8
+
9
+ export type SqlRow = Record<string, unknown>
10
+
11
+ export type SqlDb = {
12
+ readonly path?: string
13
+ all: <T extends SqlRow = SqlRow>(sql: string, ...params: unknown[]) => T[]
14
+ get: <T extends SqlRow = SqlRow>(sql: string, ...params: unknown[]) => T | null
15
+ close: () => void
16
+ }
17
+
18
+ const require = createRequire(import.meta.url)
19
+
20
+ /**
21
+ * Lock wait before a query fails with SQLITE_BUSY. Fail-fast: a transient WAL
22
+ * checkpoint lock should surface as an error row (and a `sql.busy` debug line)
23
+ * instead of blocking the whole TUI for seconds.
24
+ */
25
+ const BUSY_TIMEOUT_MS = 100
26
+
27
+ /** One-line query label for the profile log; computed lazily (profiling off = free). */
28
+ function sqlLabel(sql: string): string {
29
+ return sql.replace(/\s+/g, " ").trim().slice(0, 80)
30
+ }
31
+
32
+ /** True when a thrown error is a SQLite lock/busy failure (SQLITE_BUSY). */
33
+ export function isBusyError(e: unknown): boolean {
34
+ return e instanceof Error ? /locked|busy/i.test(e.message) : false
35
+ }
36
+
37
+ /**
38
+ * Run a query, logging a `sql.busy` debug line when the DB is locked. The
39
+ * error still propagates so the readonly request gateway can retry the whole request.
40
+ */
41
+ function guardQuery<T>(sql: string, fn: () => T): T {
42
+ try {
43
+ return fn()
44
+ } catch (e) {
45
+ if (isBusyError(e)) dbg("sql.busy", "db locked", { q: sqlLabel(sql) })
46
+ throw e
47
+ }
48
+ }
49
+
50
+ function wrapBun(dbPath: string, db: {
51
+ query: (sql: string) => { all: (...p: unknown[]) => SqlRow[]; get: (...p: unknown[]) => SqlRow | null }
52
+ exec: (sql: string) => void
53
+ close: () => void
54
+ }): SqlDb {
55
+ try {
56
+ db.exec("PRAGMA query_only = ON")
57
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`)
58
+ } catch {
59
+ // older / restricted
60
+ }
61
+ return {
62
+ path: dbPath,
63
+ all: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
64
+ profile("sql", () => guardQuery(sql, () => db.query(sql).all(...params) as T[]), () => ({ q: sqlLabel(sql) })),
65
+ get: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
66
+ profile("sql", () => guardQuery(sql, () => (db.query(sql).get(...params) as T | null) ?? null), () => ({ q: sqlLabel(sql) })),
67
+ close: () => {
68
+ try {
69
+ db.close()
70
+ } catch {
71
+ // ignore
72
+ }
73
+ },
74
+ }
75
+ }
76
+
77
+ function wrapNodeSync(dbPath: string, db: {
78
+ prepare: (sql: string) => {
79
+ all: (...p: unknown[]) => SqlRow[]
80
+ get: (...p: unknown[]) => SqlRow | undefined
81
+ }
82
+ exec: (sql: string) => void
83
+ close: () => void
84
+ }): SqlDb {
85
+ try {
86
+ db.exec("PRAGMA query_only = ON")
87
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`)
88
+ } catch {
89
+ // ignore
90
+ }
91
+ return {
92
+ path: dbPath,
93
+ all: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
94
+ profile("sql", () => guardQuery(sql, () => db.prepare(sql).all(...params) as T[]), () => ({ q: sqlLabel(sql) })),
95
+ get: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
96
+ profile("sql", () => guardQuery(sql, () => (db.prepare(sql).get(...params) as T | undefined) ?? null), () => ({ q: sqlLabel(sql) })),
97
+ close: () => {
98
+ try {
99
+ db.close()
100
+ } catch {
101
+ // ignore
102
+ }
103
+ },
104
+ }
105
+ }
106
+
107
+ let hold: { path: string; db: SqlDb } | null = null
108
+
109
+ function openFresh(dbPath: string): SqlDb | null {
110
+ if (!dbPath || !fs.existsSync(dbPath)) return null
111
+ try {
112
+ if (typeof process.versions.bun === "string") {
113
+ const { Database } = require("bun:sqlite") as {
114
+ Database: new (
115
+ path: string,
116
+ opts?: { readonly?: boolean; create?: boolean },
117
+ ) => Parameters<typeof wrapBun>[1]
118
+ }
119
+ return wrapBun(dbPath, new Database(dbPath, { readonly: true, create: false }))
120
+ }
121
+ } catch {
122
+ // try node next
123
+ }
124
+ try {
125
+ const mod = require("node:sqlite") as {
126
+ DatabaseSync: new (
127
+ path: string,
128
+ opts?: { readOnly?: boolean },
129
+ ) => Parameters<typeof wrapNodeSync>[1]
130
+ }
131
+ if (!mod?.DatabaseSync) return null
132
+ return wrapNodeSync(dbPath, new mod.DatabaseSync(dbPath, { readOnly: true }))
133
+ } catch {
134
+ return null
135
+ }
136
+ }
137
+
138
+ /** De-duplicated, non-empty string ids for `IN (...)` queries. */
139
+ export function uniqueIds(ids: readonly string[]): string[] {
140
+ return [...new Set(ids.filter((id) => typeof id === "string" && id.length > 0))]
141
+ }
142
+
143
+ /** Drop the cached handle so the next open is a real reopen. */
144
+ export function resetReadonlyDb(): void {
145
+ if (!hold) return
146
+ try {
147
+ hold.db.close()
148
+ } catch {
149
+ // ignore
150
+ }
151
+ hold = null
152
+ }
153
+
154
+ /** Open opencode.db readonly. Reuses one handle per path. Never throws — returns null on failure. */
155
+ export function openReadonlyDb(dbPath: string): SqlDb | null {
156
+ if (!dbPath) return null
157
+ if (hold?.path === dbPath) return hold.db
158
+ resetReadonlyDb()
159
+ const db = openFresh(dbPath)
160
+ if (!db) return null
161
+ hold = { path: dbPath, db }
162
+ pushScreenLine("db open")
163
+ return db
164
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * One process-local policy for readonly SQLite requests. Requests are queued,
3
+ * retried as a whole, and publish only their final state. Stages yield to the
4
+ * event loop so the synchronous SQLite driver never forms one long main-thread
5
+ * burst when the worker fallback is active.
6
+ */
7
+ import {
8
+ openReadonlyDb,
9
+ resetReadonlyDb,
10
+ type SqlDb,
11
+ } from "./pware.oc.core.sqlite.js"
12
+
13
+ export type ReadStage<T> = (db: SqlDb, state: T) => T
14
+
15
+ export type ReadRequest<T> = {
16
+ readonly dbPath: string
17
+ readonly initial: () => T
18
+ readonly stages: readonly ReadStage<T>[]
19
+ readonly fallback: (error: unknown) => T
20
+ }
21
+
22
+ export type ReadGateway = {
23
+ run: <T>(request: ReadRequest<T>) => Promise<T>
24
+ }
25
+
26
+ type ReadGatewayDeps = {
27
+ readonly open: (dbPath: string) => SqlDb | null
28
+ readonly reset: () => void
29
+ readonly yieldControl: () => Promise<void>
30
+ }
31
+
32
+ export class ReadonlyDbUnavailableError extends Error {
33
+ readonly dbPath: string
34
+
35
+ constructor(dbPath: string) {
36
+ super(`readonly SQLite unavailable: ${dbPath}`)
37
+ this.name = "ReadonlyDbUnavailableError"
38
+ this.dbPath = dbPath
39
+ }
40
+ }
41
+
42
+ function yieldToEventLoop(): Promise<void> {
43
+ return new Promise((resolve) => setTimeout(resolve, 0))
44
+ }
45
+
46
+ export function createReadGateway(
47
+ deps: ReadGatewayDeps = {
48
+ open: openReadonlyDb,
49
+ reset: resetReadonlyDb,
50
+ yieldControl: yieldToEventLoop,
51
+ },
52
+ ): ReadGateway {
53
+ let tail = Promise.resolve()
54
+
55
+ const execute = async <T>(request: ReadRequest<T>): Promise<T> => {
56
+ let firstError: unknown = null
57
+ for (let attempt = 0; attempt < 2; attempt += 1) {
58
+ try {
59
+ const db = deps.open(request.dbPath)
60
+ if (!db) throw new ReadonlyDbUnavailableError(request.dbPath)
61
+ let state = request.initial()
62
+ for (let index = 0; index < request.stages.length; index += 1) {
63
+ const stage = request.stages[index]
64
+ if (!stage) continue
65
+ if (index > 0) await deps.yieldControl()
66
+ state = stage(db, state)
67
+ }
68
+ return state
69
+ } catch (error) {
70
+ firstError ??= error
71
+ deps.reset()
72
+ }
73
+ }
74
+ return request.fallback(firstError)
75
+ }
76
+
77
+ return {
78
+ run<T>(request: ReadRequest<T>): Promise<T> {
79
+ const result = tail.then(() => execute(request))
80
+ tail = result.then(
81
+ () => undefined,
82
+ () => undefined,
83
+ )
84
+ return result
85
+ },
86
+ }
87
+ }
88
+
89
+ export const readonlyReadGateway = createReadGateway()