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.
- package/.oesignore +17 -17
- package/CHANGELOG.md +96 -95
- package/LICENSE +21 -21
- package/README.md +150 -235
- package/oes.json +13 -11
- package/package.json +100 -100
- package/scripts/install-git-hooks.mjs +29 -29
- package/src/pware.oc.core/constants/index.ts +18 -18
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -29
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +15 -13
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -241
- package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -55
- package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -65
- package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -58
- package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -63
- package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -34
- package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -74
- package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -69
- package/src/pware.oc.core/git/index.ts +7 -7
- package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -237
- package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -87
- package/src/pware.oc.core/index.ts +19 -18
- package/src/pware.oc.core/pware.oc.core.bus.ts +42 -42
- package/src/pware.oc.core/pware.oc.core.cache.ts +48 -48
- package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -97
- package/src/pware.oc.core/pware.oc.core.debug.ts +280 -280
- package/src/pware.oc.core/pware.oc.core.events.ts +79 -79
- package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -135
- package/src/pware.oc.core/pware.oc.core.layout.ts +234 -234
- package/src/pware.oc.core/pware.oc.core.oes.ts +99 -89
- package/src/pware.oc.core/pware.oc.core.paths.ts +240 -240
- package/src/pware.oc.core/pware.oc.core.preview.ts +74 -74
- package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -726
- package/src/pware.oc.core/pware.oc.core.runtimeConfig.ts +41 -0
- package/src/pware.oc.core/pware.oc.core.sqlite.ts +164 -175
- package/src/pware.oc.core/pware.oc.core.sqliteGateway.ts +89 -0
- package/src/pware.oc.core/pware.oc.core.status.ts +214 -214
- package/src/pware.oc.core/pware.oc.core.timing.ts +63 -63
- package/src/pware.oc.core/pware.oc.core.width.ts +215 -215
- package/src/pware.oc.omo/constants/index.ts +15 -15
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -18
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -49
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -29
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -11
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -60
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -49
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -25
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -38
- package/src/pware.oc.omo/index.ts +7 -7
- package/src/pware.oc.omo/resolver/index.ts +19 -19
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -179
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -116
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -34
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -503
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -40
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -269
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -155
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +363 -361
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -11
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -14
- package/src/pware.oc.opencode/constants/index.ts +10 -10
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -15
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -18
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -45
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -58
- package/src/pware.oc.opencode/index.ts +8 -8
- package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -182
- package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -399
- package/src/pware.oc.opencode/resolver/index.ts +223 -170
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +150 -162
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +149 -185
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +248 -211
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +22 -26
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +224 -236
- package/src/pware.oc.perf/index.ts +14 -14
- package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -15
- package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -284
- package/src/pware.oc.perf/pware.oc.perf.reader.ts +962 -927
- package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -88
- package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -118
- package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -79
- package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -71
- package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -214
- package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -179
- package/src/pware.oc.perf/pware.oc.perf.view.tsx +463 -462
- package/src/pware.oc.runtime/index.ts +13 -13
- package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -142
- package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +87 -79
- package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +282 -253
- package/src/pware.oc.runtime/pware.oc.runtime.omoRead.ts +37 -0
- package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +83 -95
- package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +130 -129
- package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -105
- package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +44 -43
- package/src/pware.oc.runtime/resolver/index.ts +268 -224
- package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -146
- package/src/pware.oc.ui/index.ts +12 -12
- package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +381 -339
- package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -144
- package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -111
- package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -82
- package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -843
- package/src/pware.oc.ui/pware.oc.ui.sections.tsx +552 -526
- package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1718 -1629
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
db.exec(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
profile("sql", () => guardQuery(sql, () =>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return null
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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()
|