dsh-lcx-codex 0.3.3 → 0.4.0-rc.13

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.
@@ -1,339 +0,0 @@
1
- import { convertResponsesMessages } from '@earendil-works/pi-ai/api/openai-responses-shared'
2
- import { offloadRequestImages } from '@deepseek-ai/dsh-llm'
3
-
4
- export const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024
5
- const SUPPORTED_IMAGE_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif'])
6
- const RESPONSES_TOOL_CALL_PROVIDERS = new Set(['openai', 'openai-codex', 'opencode'])
7
-
8
- function compactError(message, code, cause) {
9
- const error = new Error(message, cause === undefined ? undefined : { cause })
10
- error.code = code
11
- return error
12
- }
13
-
14
- function invalidReplay(message) {
15
- return compactError(`invalid pi-ai replay state: ${message}`, 'LCX_COMPACT_INVALID_REPLAY_STATE')
16
- }
17
-
18
- function unsupportedContent(type) {
19
- return compactError(
20
- `LCX Compact cannot safely serialize DSH message content type: ${String(type)}`,
21
- 'LCX_CHECKPOINT_PORTABLE_UNSUPPORTED_CONTENT',
22
- )
23
- }
24
-
25
- function parseArguments(raw) {
26
- try {
27
- const parsed = JSON.parse(raw)
28
- if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed
29
- } catch {
30
- // Match the DSH pi-ai adapter: malformed model arguments replay as an empty object.
31
- }
32
- return {}
33
- }
34
-
35
- function emptyUsage() {
36
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }
37
- }
38
-
39
- function replayBlockType(type) {
40
- if (type === 'text') return 'text'
41
- if (type === 'reasoning') return 'reasoning'
42
- if (type === 'tool-call') return 'tool-call'
43
- return undefined
44
- }
45
-
46
- export function readDshPiReplayState(value) {
47
- if (value === null || typeof value !== 'object' || Array.isArray(value)) throw invalidReplay('expected a replay envelope')
48
- const response = value.response
49
- if (response === null || typeof response !== 'object' || Array.isArray(response)) throw invalidReplay('expected a response object')
50
- if (response.kind !== 'pi-ai') throw invalidReplay('unknown state kind')
51
- if (response.version !== 2) throw invalidReplay(`unsupported version ${String(response.version)}`)
52
- for (const key of ['api', 'provider', 'model']) {
53
- if (typeof response[key] !== 'string' || response[key].length === 0) throw invalidReplay(`${key} must be a non-empty string`)
54
- }
55
- if (!['stop', 'length', 'toolUse', 'error', 'aborted'].includes(response.stopReason)) throw invalidReplay('unknown stopReason')
56
- if (response.responseModel !== undefined && typeof response.responseModel !== 'string') throw invalidReplay('responseModel must be a string')
57
- if (response.responseId !== undefined && typeof response.responseId !== 'string') throw invalidReplay('responseId must be a string')
58
- if (!Array.isArray(value.blocks)) throw invalidReplay('blocks must be an array')
59
- for (const [index, block] of value.blocks.entries()) {
60
- if (block === null || typeof block !== 'object' || Array.isArray(block)) throw invalidReplay(`block ${index} must be an object`)
61
- if (!['text', 'reasoning', 'tool-call'].includes(block.type)) throw invalidReplay(`block ${index} has an unknown type`)
62
- for (const signature of ['textSignature', 'thinkingSignature', 'thoughtSignature']) {
63
- if (block[signature] !== undefined && typeof block[signature] !== 'string') {
64
- throw invalidReplay(`block ${index} ${signature} must be a string`)
65
- }
66
- }
67
- if (block.redacted !== undefined && typeof block.redacted !== 'boolean') throw invalidReplay(`block ${index} redacted must be boolean`)
68
- }
69
- return { response, blocks: value.blocks }
70
- }
71
-
72
- function foreignAssistant(message) {
73
- const source = message?.source?.kind === 'model' ? message.source : undefined
74
- const content = []
75
- for (const block of message.content ?? []) {
76
- if (block?.type === 'text') content.push({ type: 'text', text: block.text })
77
- else if (block?.type === 'reasoning') content.push({ type: 'thinking', thinking: block.text })
78
- else if (block?.type === 'tool-call') {
79
- content.push({ type: 'toolCall', id: String(block.id), name: String(block.name), arguments: parseArguments(block.arguments) })
80
- } else if (block?.type === 'image') {
81
- throw unsupportedContent('assistant image')
82
- } else {
83
- throw unsupportedContent(block?.type)
84
- }
85
- }
86
- return {
87
- role: 'assistant',
88
- content,
89
- api: 'dsh-foreign',
90
- provider: source?.provider ?? 'dsh-foreign',
91
- model: source?.model ?? 'dsh-foreign',
92
- usage: emptyUsage(),
93
- stopReason: content.some((block) => block.type === 'toolCall') ? 'toolUse' : 'stop',
94
- timestamp: 0,
95
- }
96
- }
97
-
98
- function replayedAssistant(message, source) {
99
- const state = readDshPiReplayState(source.replayState)
100
- if (state.response.provider !== source.provider) throw invalidReplay('provider does not match assistant source')
101
- if (state.response.model !== source.model) throw invalidReplay('model does not match assistant source')
102
- if (state.blocks.length !== message.content.length) throw invalidReplay('block count does not match assistant content')
103
- const content = message.content.map((block, index) => {
104
- const replay = state.blocks[index]
105
- if (replayBlockType(block?.type) !== replay?.type) throw invalidReplay(`block ${index} does not match assistant content`)
106
- if (block.type === 'text') {
107
- return { type: 'text', text: block.text, ...(replay.textSignature === undefined ? {} : { textSignature: replay.textSignature }) }
108
- }
109
- if (block.type === 'reasoning') {
110
- return {
111
- type: 'thinking',
112
- thinking: block.text,
113
- ...(replay.thinkingSignature === undefined ? {} : { thinkingSignature: replay.thinkingSignature }),
114
- ...(replay.redacted === undefined ? {} : { redacted: replay.redacted }),
115
- }
116
- }
117
- return {
118
- type: 'toolCall',
119
- id: String(block.id),
120
- name: String(block.name),
121
- arguments: parseArguments(block.arguments),
122
- ...(replay.thoughtSignature === undefined ? {} : { thoughtSignature: replay.thoughtSignature }),
123
- }
124
- })
125
- return {
126
- role: 'assistant',
127
- content,
128
- api: state.response.api,
129
- provider: state.response.provider,
130
- model: state.response.model,
131
- ...(state.response.responseModel === undefined ? {} : { responseModel: state.response.responseModel }),
132
- ...(state.response.responseId === undefined ? {} : { responseId: state.response.responseId }),
133
- usage: emptyUsage(),
134
- stopReason: state.response.stopReason,
135
- timestamp: 0,
136
- }
137
- }
138
-
139
- function toPiAssistant(message, onReplayDegrade) {
140
- const source = message?.source
141
- if (source?.kind !== 'model' || source.replayState === undefined) return foreignAssistant(message)
142
- try {
143
- return replayedAssistant(message, source)
144
- } catch (error) {
145
- if (error?.code !== 'LCX_COMPACT_INVALID_REPLAY_STATE') throw error
146
- onReplayDegrade?.(error.message)
147
- return foreignAssistant(message)
148
- }
149
- }
150
-
151
- function hasImageBlocks(blocks) {
152
- return (blocks ?? []).some((block) => block?.type === 'image' || (block?.type === 'tool-result' && hasImageBlocks(block.content)))
153
- }
154
-
155
- export async function resolveDshImage(block, options = {}) {
156
- if (typeof options.resolveImage !== 'function') {
157
- throw compactError('LCX Compact cannot resolve image attachment without the DSH attachment service', 'LCX_COMPACT_IMAGE_UNAVAILABLE')
158
- }
159
- let stored
160
- try {
161
- stored = await options.resolveImage(block, options.signal)
162
- } catch (error) {
163
- if (error?.code?.startsWith?.('LCX_COMPACT_IMAGE_')) throw error
164
- throw compactError('LCX Compact failed to read image attachment', 'LCX_COMPACT_IMAGE_UNAVAILABLE', error)
165
- }
166
- const data = stored?.data
167
- const mediaType = String(stored?.mediaType ?? stored?.ref?.mediaType ?? block?.attachment?.mediaType ?? '').toLowerCase()
168
- const bytes = data?.byteLength ?? data?.length
169
- if (!SUPPORTED_IMAGE_MEDIA_TYPES.has(mediaType)) {
170
- throw compactError(`LCX Compact does not support image media type: ${mediaType || 'missing'}`, 'LCX_COMPACT_IMAGE_UNSUPPORTED')
171
- }
172
- if (!Number.isSafeInteger(bytes) || bytes <= 0) {
173
- throw compactError('LCX Compact received empty or invalid image attachment data', 'LCX_COMPACT_IMAGE_UNAVAILABLE')
174
- }
175
- const base64 = Buffer.from(data).toString('base64')
176
- const maxBytes = options.maxRequestImageBytes ?? DEFAULT_MAX_REQUEST_IMAGE_BYTES
177
- if (base64.length > maxBytes) {
178
- throw compactError(`LCX Compact image attachment exceeds the ${maxBytes}-byte base64 payload limit`, 'LCX_COMPACT_IMAGE_TOO_LARGE')
179
- }
180
- const imageUrl = `data:${mediaType};base64,${base64}`
181
- options.onImageResolved?.({ imageUrl, attachment: structuredClone(stored?.ref ?? block.attachment) })
182
- return { type: 'image', data: base64, mimeType: mediaType }
183
- }
184
-
185
- async function piUserContent(blocks, options) {
186
- const content = []
187
- for (const block of blocks ?? []) {
188
- if (block?.type === 'text') {
189
- if (block.text.length > 0) content.push({ type: 'text', text: block.text })
190
- } else if (block?.type === 'reasoning') {
191
- continue
192
- } else if (block?.type === 'image') {
193
- if (options.imageSupport === 'unsupported') {
194
- content.push({ type: 'image', data: '', mimeType: block?.attachment?.mediaType ?? 'image/png' })
195
- } else {
196
- content.push(await resolveDshImage(block, options))
197
- }
198
- } else if (block?.type === 'tool-result') {
199
- const nested = await piUserContent(block.content, options)
200
- if (typeof nested === 'string') {
201
- if (nested.length > 0) content.push({ type: 'text', text: nested })
202
- } else {
203
- content.push(...nested)
204
- }
205
- } else {
206
- throw unsupportedContent(block?.type)
207
- }
208
- }
209
- if (content.every((block) => block.type === 'text')) return content.map((block) => block.text).join('')
210
- return content
211
- }
212
-
213
- async function dshToPiContext(messages, options) {
214
- const converted = []
215
- const toolNames = new Map()
216
- for (const message of messages ?? []) {
217
- if (message === null || typeof message !== 'object' || Array.isArray(message) || !Array.isArray(message.content)) {
218
- throw unsupportedContent('message')
219
- }
220
- if (message.role === 'system') {
221
- if (hasImageBlocks(message.content)) throw unsupportedContent('system image')
222
- const text = message.content.filter((block) => block?.type === 'text').map((block) => block.text).join('')
223
- if (message.content.some((block) => block?.type !== 'text' && block?.type !== 'reasoning')) throw unsupportedContent('system block')
224
- if (text) converted.push({ role: 'user', content: text, timestamp: 0 })
225
- continue
226
- }
227
- if (message.role === 'assistant') {
228
- const assistant = toPiAssistant(message, options.onReplayDegrade)
229
- for (const block of assistant.content) {
230
- if (block.type === 'toolCall') toolNames.set(block.id, block.name)
231
- }
232
- converted.push(assistant)
233
- continue
234
- }
235
- if (message.role !== 'user') throw unsupportedContent(`message role ${String(message.role)}`)
236
- const ordinary = message.content.filter((block) => block?.type !== 'tool-result')
237
- if (ordinary.some((block) => !['text', 'reasoning', 'image'].includes(block?.type))) throw unsupportedContent(ordinary.find((block) => !['text', 'reasoning', 'image'].includes(block?.type))?.type)
238
- const content = await piUserContent(ordinary, options)
239
- const results = message.content.filter((block) => block?.type === 'tool-result')
240
- if ((typeof content === 'string' ? content.length > 0 : content.length > 0) || results.length === 0) {
241
- converted.push({ role: 'user', content, timestamp: 0 })
242
- }
243
- for (const result of results) {
244
- const resultContent = await piUserContent(result.content, options)
245
- converted.push({
246
- role: 'toolResult',
247
- toolCallId: String(result.toolCallId),
248
- toolName: toolNames.get(String(result.toolCallId)) ?? 'unknown',
249
- content: typeof resultContent === 'string'
250
- ? [{ type: 'text', text: resultContent || '(no output)' }]
251
- : resultContent,
252
- isError: result.isError ?? false,
253
- timestamp: 0,
254
- })
255
- }
256
- }
257
- return { messages: converted }
258
- }
259
-
260
- export async function resolveModelImageSupport(llm, route, signal) {
261
- if (typeof llm?.resolveModelInfo !== 'function') return 'unknown'
262
- try {
263
- const model = await llm.resolveModelInfo(route.provider, route.model, signal)
264
- if (!Array.isArray(model?.inputModalities)) return 'unknown'
265
- return model.inputModalities.includes('image') ? 'supported' : 'unsupported'
266
- } catch {
267
- return 'unknown'
268
- }
269
- }
270
-
271
- export async function serializeDshResponsesInput(messages, options = {}) {
272
- const route = options.route ?? {}
273
- const imageSupport = options.imageSupport ?? 'unknown'
274
- if (!['supported', 'unsupported', 'unknown'].includes(imageSupport)) {
275
- throw compactError(`Invalid LCX Compact image capability: ${String(imageSupport)}`, 'LCX_COMPACT_IMAGE_CAPABILITY_UNKNOWN')
276
- }
277
- if (imageSupport === 'unknown' && (messages ?? []).some((message) => hasImageBlocks(message?.content))) {
278
- throw compactError('LCX Compact cannot determine whether the target model accepts images', 'LCX_COMPACT_IMAGE_CAPABILITY_UNKNOWN')
279
- }
280
- const context = await dshToPiContext(messages, { ...options, imageSupport })
281
- const model = {
282
- id: String(route.model ?? ''),
283
- name: String(route.model ?? ''),
284
- api: 'openai-responses',
285
- provider: String(route.provider ?? ''),
286
- baseUrl: String(route.baseURL ?? ''),
287
- input: imageSupport === 'supported' ? ['text', 'image'] : ['text'],
288
- reasoning: true,
289
- contextWindow: 1,
290
- maxTokens: 1,
291
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
292
- }
293
- const allowedToolCallProviders = new Set([...RESPONSES_TOOL_CALL_PROVIDERS, model.provider])
294
- const converted = convertResponsesMessages(model, context, allowedToolCallProviders, { includeSystemPrompt: false })
295
- // Pi emits user messages in the SDK shorthand `{ role, content }`. DSH's
296
- // checkpoint/store contract is the explicit Responses item shape, so make
297
- // that boundary canonical without changing Pi's internal serializer.
298
- const stripImageDetail = (value) => {
299
- if (!Array.isArray(value)) return value
300
- return value.map((part) => {
301
- if (part?.type === 'input_image') {
302
- const { detail: _detail, ...withoutDetail } = part
303
- return withoutDetail
304
- }
305
- return part
306
- })
307
- }
308
- return converted.map((item) => {
309
- if (item?.role === 'user') {
310
- const content = typeof item.content === 'string'
311
- ? [{ type: 'input_text', text: item.content }]
312
- : item.content
313
- return { ...(item.type === undefined ? { type: 'message' } : {}), role: 'user', content: stripImageDetail(content) }
314
- }
315
- if (item?.type === 'function_call_output') return { ...item, output: stripImageDetail(item.output) }
316
- return item
317
- })
318
- }
319
-
320
- function hasCompleteImageMetadata(messages) {
321
- const complete = (blocks) => blocks.every((block) => {
322
- if (block.type === 'image') {
323
- const bytes = Number(block.attachment?.bytes)
324
- return Number.isSafeInteger(bytes) && bytes > 0
325
- }
326
- return block.type !== 'tool-result' || complete(block.content)
327
- })
328
- return messages.every((message) => complete(message.content))
329
- }
330
-
331
- export function offloadDshRequestImages(messages, maxRequestImageBytes = DEFAULT_MAX_REQUEST_IMAGE_BYTES) {
332
- if (!Number.isSafeInteger(maxRequestImageBytes) || maxRequestImageBytes <= 0) {
333
- throw compactError('LCX Compact maxRequestImageBytes must be a positive integer', 'LCX_COMPACT_IMAGE_LIMIT_INVALID')
334
- }
335
- const input = messages ?? []
336
- return hasCompleteImageMetadata(input)
337
- ? offloadRequestImages(input, maxRequestImageBytes)
338
- : input
339
- }
@@ -1,18 +0,0 @@
1
- import { execFileSync } from 'node:child_process'
2
-
3
- export function ensurePrivateFileAcl(target) {
4
- if (process.platform !== 'win32') return true
5
- try {
6
- const identity = execFileSync('whoami.exe', ['/user', '/fo', 'csv', '/nh'], {
7
- encoding: 'utf8', timeout: 3000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'],
8
- }).match(/S-1-[0-9-]+/iu)?.[0]
9
- if (!identity) return false
10
- const sids = [identity, 'S-1-5-18', 'S-1-5-32-544'].map((sid) => `*${sid}:F`)
11
- execFileSync('icacls.exe', [target, '/inheritance:r', '/grant:r', ...sids], {
12
- encoding: 'utf8', timeout: 5000, windowsHide: true, stdio: 'ignore',
13
- })
14
- return true
15
- } catch {
16
- return false
17
- }
18
- }
@@ -1,70 +0,0 @@
1
- const STALE_SESSION_CODE = 'LCX_SESSION_GENERATION_STALE'
2
-
3
- function sessionIdOf(session) {
4
- const value = session?.id
5
- return typeof value === 'string' && value.length > 0 ? value : undefined
6
- }
7
-
8
- function staleSessionError(sessionId, phase) {
9
- const error = new Error(`LCX session generation became stale during ${phase}: ${sessionId}`)
10
- error.code = STALE_SESSION_CODE
11
- return error
12
- }
13
-
14
- /**
15
- * Track the public DSH session lifecycle without depending on private agent-loop APIs.
16
- * A lease is intentionally fail-closed when a session service exists but the id is
17
- * unknown; callers without a session id (or without the optional service) retain
18
- * the pre-lifecycle behavior.
19
- */
20
- export function createSessionGenerationTracker(ctx) {
21
- const sessions = ctx?.get?.('sessions') ?? ctx?.sessions
22
- const enabled = Boolean(sessions && typeof ctx?.on === 'function')
23
- const states = new Map()
24
- const disposers = []
25
-
26
- const created = (session) => {
27
- const id = sessionIdOf(session)
28
- if (!id) return
29
- const previous = states.get(id)
30
- states.set(id, { generation: (previous?.generation ?? 0) + 1, active: true })
31
- }
32
- const disposed = (session) => {
33
- const id = sessionIdOf(session)
34
- if (!id) return
35
- const previous = states.get(id)
36
- states.set(id, { generation: (previous?.generation ?? 0) + 1, active: false })
37
- }
38
-
39
- if (enabled) {
40
- for (const session of sessions.list?.() ?? []) created(session)
41
- for (const [event, handler] of [['session/created', created], ['session/disposed', disposed]]) {
42
- const dispose = ctx.on(event, handler)
43
- if (typeof dispose === 'function') disposers.push(dispose)
44
- }
45
- }
46
-
47
- return {
48
- capture(sessionId) {
49
- const id = typeof sessionId === 'string' ? sessionId : ''
50
- if (!enabled || !id) return { assert() {} }
51
- const captured = states.get(id)
52
- const generation = captured?.generation ?? 0
53
- return {
54
- id,
55
- generation,
56
- assert(phase = 'commit') {
57
- const current = states.get(id)
58
- if (!current || !current.active || current.generation !== generation) {
59
- throw staleSessionError(id, phase)
60
- }
61
- },
62
- }
63
- },
64
- dispose() {
65
- while (disposers.length > 0) disposers.pop()?.()
66
- },
67
- }
68
- }
69
-
70
- export { STALE_SESSION_CODE }
@@ -1,181 +0,0 @@
1
- import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
2
- import { basename, dirname, join } from 'node:path'
3
- import { randomUUID } from 'node:crypto'
4
- import { ensurePrivateFileAcl } from './private-file.js'
5
-
6
- const LOCK_STALE_MS = 30 * 1000
7
- const LOCK_WAIT_MS = 5 * 1000
8
-
9
- function sleepSync(milliseconds) {
10
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds)
11
- }
12
-
13
- function processAlive(pid) {
14
- if (!Number.isSafeInteger(pid) || pid <= 0) return false
15
- try {
16
- process.kill(pid, 0)
17
- return true
18
- } catch (error) {
19
- return error?.code === 'EPERM'
20
- }
21
- }
22
-
23
- function staleLockSnapshot(file) {
24
- try {
25
- const info = statSync(file)
26
- const text = readFileSync(file, 'utf8')
27
- const [pidText, startedAtText] = text.split(/\r?\n/u)
28
- const pid = Number(pidText)
29
- const startedAt = Number(startedAtText)
30
- const age = Date.now() - Math.max(info.mtimeMs, Number.isFinite(startedAt) ? startedAt : 0)
31
- if (age <= LOCK_STALE_MS || processAlive(pid)) return undefined
32
- return { text, mtimeMs: info.mtimeMs, size: info.size }
33
- } catch {
34
- return undefined
35
- }
36
- }
37
-
38
- function unlinkLockIfUnchanged(file, snapshot) {
39
- if (!snapshot) return false
40
- try {
41
- const info = statSync(file)
42
- if (info.mtimeMs !== snapshot.mtimeMs || info.size !== snapshot.size || readFileSync(file, 'utf8') !== snapshot.text) return false
43
- unlinkSync(file)
44
- return true
45
- } catch {
46
- return false
47
- }
48
- }
49
-
50
- function signature(file) {
51
- try {
52
- const value = statSync(file)
53
- return `${value.mtimeMs}:${value.ctimeMs}:${value.size}`
54
- } catch (error) {
55
- if (error?.code === 'ENOENT') return undefined
56
- throw error
57
- }
58
- }
59
-
60
- export class AtomicWebSearchStore {
61
- constructor(file, { empty, validate, corruptCode, writeCode, maxBytes = 2 * 1024 * 1024 }) {
62
- this.file = file
63
- this.lockFile = `${file}.lock`
64
- this.empty = empty
65
- this.validate = validate
66
- this.corruptCode = corruptCode
67
- this.writeCode = writeCode
68
- this.maxBytes = maxBytes
69
- this.fileSignature = undefined
70
- this.data = this.load()
71
- }
72
-
73
- error(message, code, cause) {
74
- const error = new Error(message, cause === undefined ? undefined : { cause })
75
- error.code = code
76
- return error
77
- }
78
-
79
- load(repairAcl = true) {
80
- try {
81
- const text = readFileSync(this.file, 'utf8')
82
- if (Buffer.byteLength(text, 'utf8') > this.maxBytes) throw this.error(`Web Search store exceeds ${this.maxBytes} bytes`, this.corruptCode)
83
- const parsed = JSON.parse(text)
84
- if (!this.validate(parsed)) throw this.error(`Invalid Web Search store ${this.file}`, this.corruptCode)
85
- this.fileSignature = signature(this.file)
86
- return parsed
87
- } catch (error) {
88
- if (error?.code === 'ENOENT') {
89
- this.fileSignature = undefined
90
- return this.empty()
91
- }
92
- if (repairAcl && ['EACCES', 'EPERM'].includes(error?.code) && ensurePrivateFileAcl(this.file)) {
93
- return this.load(false)
94
- }
95
- if (error?.code === this.corruptCode) throw error
96
- throw this.error(`Unable to load Web Search store ${this.file}`, this.corruptCode, error)
97
- }
98
- }
99
-
100
- refresh() {
101
- if (signature(this.file) !== this.fileSignature) this.data = this.load()
102
- }
103
-
104
- update(mutator) {
105
- mkdirSync(dirname(this.file), { recursive: true })
106
- ensurePrivateFileAcl(dirname(this.file))
107
- const release = this.acquireLock()
108
- try {
109
- const current = this.load()
110
- const next = mutator(structuredClone(current))
111
- this.saveUnlocked(next)
112
- return structuredClone(next)
113
- } finally {
114
- release()
115
- }
116
- }
117
-
118
- acquireLock() {
119
- const started = Date.now()
120
- while (Date.now() - started <= LOCK_WAIT_MS) {
121
- const token = randomUUID()
122
- const lockText = `${process.pid}\n${Date.now()}\n${token}\n`
123
- let fd
124
- let created = false
125
- try {
126
- fd = openSync(this.lockFile, 'wx', 0o600)
127
- created = true
128
- if (!ensurePrivateFileAcl(this.lockFile)) throw new Error(`Unable to apply private ACL to Web Search store lock ${this.lockFile}`)
129
- writeFileSync(fd, lockText, { encoding: 'utf8' })
130
- fsyncSync(fd)
131
- closeSync(fd)
132
- fd = undefined
133
- return () => {
134
- try {
135
- if (readFileSync(this.lockFile, 'utf8') === lockText) unlinkSync(this.lockFile)
136
- } catch {
137
- // A crashed owner may have left a lock that was recovered by another writer.
138
- }
139
- }
140
- } catch (error) {
141
- if (fd !== undefined) {
142
- try { closeSync(fd) } catch {}
143
- }
144
- if (created) {
145
- try { unlinkSync(this.lockFile) } catch {}
146
- }
147
- if (error?.code !== 'EEXIST') throw this.error(`Unable to acquire Web Search store lock ${this.lockFile}`, this.writeCode, error)
148
- if (unlinkLockIfUnchanged(this.lockFile, staleLockSnapshot(this.lockFile))) continue
149
- sleepSync(10)
150
- }
151
- }
152
- throw this.error(`Timed out acquiring Web Search store lock ${this.lockFile}`, this.writeCode)
153
- }
154
-
155
- saveUnlocked(data) {
156
- if (!this.validate(data)) throw this.error(`Refusing to write invalid Web Search store ${this.file}`, this.writeCode)
157
- const serialized = JSON.stringify(data, null, 2)
158
- if (Buffer.byteLength(serialized, 'utf8') > this.maxBytes) throw this.error(`Web Search store exceeds ${this.maxBytes} bytes`, this.writeCode)
159
- const directory = dirname(this.file)
160
- const temporary = join(directory, `.${basename(this.file)}.${randomUUID()}.tmp`)
161
- let fd
162
- try {
163
- fd = openSync(temporary, 'wx', 0o600)
164
- if (!ensurePrivateFileAcl(temporary)) throw new Error(`Unable to apply private ACL to Web Search store temporary file ${temporary}`)
165
- writeFileSync(fd, serialized, { encoding: 'utf8' })
166
- fsyncSync(fd)
167
- closeSync(fd)
168
- fd = undefined
169
- renameSync(temporary, this.file)
170
- ensurePrivateFileAcl(this.file)
171
- this.fileSignature = signature(this.file)
172
- this.data = data
173
- } catch (error) {
174
- if (fd !== undefined) {
175
- try { closeSync(fd) } catch {}
176
- }
177
- try { unlinkSync(temporary) } catch {}
178
- throw this.error(`Unable to atomically write Web Search store ${this.file}`, this.writeCode, error)
179
- }
180
- }
181
- }