dsh-conversation-recycle-bin 0.2.0 → 0.2.2
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/AUDIT-2026-10-17-desktop-web.md +333 -0
- package/CHANGELOG.md +57 -0
- package/INSTALL.md +2 -1
- package/README.md +16 -12
- package/client.js +197 -26
- package/config.schema.json +1 -1
- package/lib/core/cascade.js +53 -19
- package/lib/core/config.js +4 -1
- package/lib/core/errors.js +5 -2
- package/lib/core/file-lock.js +151 -0
- package/lib/core/memory/generic-json.js +2 -2
- package/lib/core/memory/json-index-adapter.js +182 -87
- package/lib/core/memory/lingshu.js +10 -4
- package/lib/core/memory/none.js +4 -2
- package/lib/core/memory/registry.js +9 -6
- package/lib/core/policy.js +4 -4
- package/lib/core/redact.js +66 -0
- package/lib/host/api.js +252 -55
- package/lib/host/catalog.js +101 -36
- package/lib/host/gateway.js +19 -5
- package/lib/host/paths.js +13 -2
- package/lib/version.js +1 -1
- package/package.json +11 -3
- package/locale/en.json +0 -63
- package/locale/zh.json +0 -63
package/lib/core/cascade.js
CHANGED
|
@@ -78,6 +78,7 @@ export async function performSessionDelete({
|
|
|
78
78
|
config,
|
|
79
79
|
now = Date.now,
|
|
80
80
|
actor = 'local-user',
|
|
81
|
+
source = 'web-ui',
|
|
81
82
|
requestId = null,
|
|
82
83
|
}) {
|
|
83
84
|
const startedAt = now()
|
|
@@ -142,10 +143,21 @@ export async function performSessionDelete({
|
|
|
142
143
|
name: 'messages',
|
|
143
144
|
run: async () => {
|
|
144
145
|
const moved = await ops.sessionLog.moveToTrash(session, dir)
|
|
146
|
+
try {
|
|
147
|
+
// Record the move immediately. A crash between the move and the
|
|
148
|
+
// final manifest write used to leave a `pending` entry that knew
|
|
149
|
+
// nothing about the log sitting in its directory: discard refused
|
|
150
|
+
// it, restore had nothing to restore, and the payload was stranded.
|
|
151
|
+
await trash.update(entry.trashId, { sessionLog: moved })
|
|
152
|
+
} catch (error) {
|
|
153
|
+
await ops.sessionLog.restoreEntry(moved).catch(() => {})
|
|
154
|
+
throw error
|
|
155
|
+
}
|
|
145
156
|
entry.sessionLog = moved
|
|
146
157
|
return async () => {
|
|
147
158
|
await ops.sessionLog.restoreEntry(moved)
|
|
148
159
|
entry.sessionLog = null
|
|
160
|
+
await trash.update(entry.trashId, { sessionLog: null }).catch(() => {})
|
|
149
161
|
}
|
|
150
162
|
},
|
|
151
163
|
})
|
|
@@ -190,7 +202,7 @@ export async function performSessionDelete({
|
|
|
190
202
|
actor,
|
|
191
203
|
requestId,
|
|
192
204
|
durationMs: now() - startedAt,
|
|
193
|
-
source
|
|
205
|
+
source,
|
|
194
206
|
})
|
|
195
207
|
},
|
|
196
208
|
})
|
|
@@ -226,7 +238,7 @@ export async function performSessionDelete({
|
|
|
226
238
|
})),
|
|
227
239
|
},
|
|
228
240
|
durationMs: now() - startedAt,
|
|
229
|
-
source
|
|
241
|
+
source,
|
|
230
242
|
})
|
|
231
243
|
.catch(() => {})
|
|
232
244
|
throw toDeleteError(result, { sessionId: session.id, title: entry.title })
|
|
@@ -263,26 +275,34 @@ const RESTORABLE_STATUSES = new Set(['trashed', 'pending', 'failed', 'restoring'
|
|
|
263
275
|
* @param {string} [input.actor]
|
|
264
276
|
* @param {string} [input.requestId]
|
|
265
277
|
*/
|
|
266
|
-
export async function performRestore({ entry, ops, trash, audit, now = Date.now, actor = 'local-user', requestId = null }) {
|
|
278
|
+
export async function performRestore({ entry, ops, trash, audit, now = Date.now, actor = 'local-user', source = 'web-ui', requestId = null }) {
|
|
267
279
|
const previousStatus = entry.status
|
|
268
280
|
if (!RESTORABLE_STATUSES.has(previousStatus)) {
|
|
269
281
|
throw new DeleteError('E_TRASH_CONFLICT', 'This recycle-bin entry is not in a restorable state.', { trashId: entry.trashId, status: previousStatus })
|
|
270
282
|
}
|
|
271
|
-
const
|
|
283
|
+
const sessionLog = entry.sessionLog !== null && entry.sessionLog !== undefined && typeof entry.sessionLog === 'object'
|
|
284
|
+
? entry.sessionLog
|
|
285
|
+
: null
|
|
272
286
|
const catalog = entry.catalog !== null && entry.catalog !== undefined && typeof entry.catalog === 'object' ? entry.catalog : null
|
|
273
287
|
const hasMemory = entry.includeMemory === true && entry.memory !== null && typeof entry.memory === 'object' && (entry.memory.total ?? 0) > 0
|
|
274
|
-
|
|
288
|
+
const canStat = typeof ops.pathExists === 'function'
|
|
289
|
+
const logInTrash = sessionLog !== null && (!canStat || await ops.pathExists(sessionLog.to))
|
|
290
|
+
const logInPlace = sessionLog !== null && canStat && await ops.pathExists(sessionLog.from)
|
|
291
|
+
const logMissing = sessionLog !== null && !logInTrash && !logInPlace
|
|
292
|
+
// Re-attaching a hard-deleted conversation whose log is nowhere would leave a
|
|
275
293
|
// conversation with no messages on disk, so that combination stays refused.
|
|
276
|
-
|
|
294
|
+
// A log that is already back in place is fine: an interrupted purge can leave
|
|
295
|
+
// exactly that state.
|
|
296
|
+
if (catalog !== null && entry.mode === 'hard' && (sessionLog === null || logMissing)) {
|
|
277
297
|
throw new DeleteError('E_TRASH_CONFLICT', 'The recycle-bin entry has no session-log location; restore is unsafe.', { trashId: entry.trashId })
|
|
278
298
|
}
|
|
279
299
|
|
|
280
300
|
const steps = []
|
|
281
|
-
if (entry.mode === 'hard' &&
|
|
301
|
+
if (entry.mode === 'hard' && logInTrash) {
|
|
282
302
|
steps.push({
|
|
283
303
|
name: 'messages',
|
|
284
304
|
run: async () => {
|
|
285
|
-
const undo = await ops.sessionLog.restoreEntry(
|
|
305
|
+
const undo = await ops.sessionLog.restoreEntry(sessionLog)
|
|
286
306
|
return async () => {
|
|
287
307
|
await undo()
|
|
288
308
|
}
|
|
@@ -314,10 +334,15 @@ export async function performRestore({ entry, ops, trash, audit, now = Date.now,
|
|
|
314
334
|
})
|
|
315
335
|
}
|
|
316
336
|
if (steps.length === 0) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
337
|
+
// The log is already back on disk (an interrupted purge can leave the
|
|
338
|
+
// manifest pointing at a log that was moved back): clearing the entry is
|
|
339
|
+
// the only remaining action, and it is safe.
|
|
340
|
+
if (!(entry.mode === 'hard' && logInPlace)) {
|
|
341
|
+
throw new DeleteError('E_TRASH_CONFLICT', 'The recycle-bin entry has nothing left to restore; discard it to reclaim the space.', {
|
|
342
|
+
trashId: entry.trashId,
|
|
343
|
+
status: previousStatus,
|
|
344
|
+
})
|
|
345
|
+
}
|
|
321
346
|
}
|
|
322
347
|
|
|
323
348
|
const startedAt = now()
|
|
@@ -340,7 +365,7 @@ export async function performRestore({ entry, ops, trash, audit, now = Date.now,
|
|
|
340
365
|
requestId,
|
|
341
366
|
failedStep: result.failedStep,
|
|
342
367
|
rollback: { ok: result.rolledBack, errors: result.rollbackErrors.map((item) => ({ step: item.step, message: String(item.error) })) },
|
|
343
|
-
source
|
|
368
|
+
source,
|
|
344
369
|
})
|
|
345
370
|
.catch(() => {})
|
|
346
371
|
throw toDeleteError(result, { sessionId: entry.sessionId, title: entry.title })
|
|
@@ -356,7 +381,7 @@ export async function performRestore({ entry, ops, trash, audit, now = Date.now,
|
|
|
356
381
|
actor,
|
|
357
382
|
requestId,
|
|
358
383
|
durationMs: now() - startedAt,
|
|
359
|
-
source
|
|
384
|
+
source,
|
|
360
385
|
})
|
|
361
386
|
await trash.remove(entry.trashId).catch(() => {})
|
|
362
387
|
return { trashId: entry.trashId, sessionId: entry.sessionId }
|
|
@@ -379,7 +404,7 @@ export async function performRestore({ entry, ops, trash, audit, now = Date.now,
|
|
|
379
404
|
* @param {string} [input.actor]
|
|
380
405
|
* @param {string} [input.requestId]
|
|
381
406
|
*/
|
|
382
|
-
export async function performPurge({ entry, ops, trash, audit, now = Date.now, actor = 'local-user', requestId = null }) {
|
|
407
|
+
export async function performPurge({ entry, ops, trash, audit, now = Date.now, actor = 'local-user', source = 'web-ui', requestId = null }) {
|
|
383
408
|
if (!['trashed', 'restored'].includes(entry.status)) {
|
|
384
409
|
throw new DeleteError('E_TRASH_CONFLICT', 'This recycle-bin entry cannot be purged in its current state.', { trashId: entry.trashId, status: entry.status })
|
|
385
410
|
}
|
|
@@ -405,8 +430,17 @@ export async function performPurge({ entry, ops, trash, audit, now = Date.now, a
|
|
|
405
430
|
name: 'messages',
|
|
406
431
|
run: async () => {
|
|
407
432
|
const moved = await ops.sessionLog.moveToTrash(session, trash.entryDir(entry.trashId))
|
|
433
|
+
try {
|
|
434
|
+
await trash.update(entry.trashId, { sessionLog: moved })
|
|
435
|
+
} catch (error) {
|
|
436
|
+
await ops.sessionLog.restoreEntry(moved).catch(() => {})
|
|
437
|
+
throw error
|
|
438
|
+
}
|
|
439
|
+
entry.sessionLog = moved
|
|
408
440
|
return async () => {
|
|
409
441
|
await ops.sessionLog.restoreEntry(moved)
|
|
442
|
+
entry.sessionLog = null
|
|
443
|
+
await trash.update(entry.trashId, { sessionLog: null }).catch(() => {})
|
|
410
444
|
}
|
|
411
445
|
},
|
|
412
446
|
})
|
|
@@ -445,7 +479,7 @@ export async function performPurge({ entry, ops, trash, audit, now = Date.now, a
|
|
|
445
479
|
requestId,
|
|
446
480
|
failedStep: result.failedStep,
|
|
447
481
|
rollback: { ok: result.rolledBack, errors: result.rollbackErrors.map((item) => ({ step: item.step, message: String(item.error) })) },
|
|
448
|
-
source
|
|
482
|
+
source,
|
|
449
483
|
})
|
|
450
484
|
.catch(() => {})
|
|
451
485
|
throw toDeleteError(result, { sessionId: entry.sessionId, title: entry.title })
|
|
@@ -461,7 +495,7 @@ export async function performPurge({ entry, ops, trash, audit, now = Date.now, a
|
|
|
461
495
|
actor,
|
|
462
496
|
requestId,
|
|
463
497
|
durationMs: now() - startedAt,
|
|
464
|
-
source
|
|
498
|
+
source,
|
|
465
499
|
})
|
|
466
500
|
return { trashId: entry.trashId, sessionId: entry.sessionId }
|
|
467
501
|
}
|
|
@@ -484,7 +518,7 @@ export async function performPurge({ entry, ops, trash, audit, now = Date.now, a
|
|
|
484
518
|
* @param {string} [input.actor]
|
|
485
519
|
* @param {string} [input.requestId]
|
|
486
520
|
*/
|
|
487
|
-
export async function performDiscard({ entry, ops, trash, audit, now = Date.now, actor = 'local-user', requestId = null }) {
|
|
521
|
+
export async function performDiscard({ entry, ops, trash, audit, now = Date.now, actor = 'local-user', source = 'web-ui', requestId = null }) {
|
|
488
522
|
if (entry.status === 'trashed') {
|
|
489
523
|
throw new DeleteError('E_TRASH_CONFLICT', 'This entry is a committed deletion; restore or purge it instead of discarding it.', {
|
|
490
524
|
trashId: entry.trashId,
|
|
@@ -524,7 +558,7 @@ export async function performDiscard({ entry, ops, trash, audit, now = Date.now,
|
|
|
524
558
|
actor,
|
|
525
559
|
requestId,
|
|
526
560
|
durationMs: now() - startedAt,
|
|
527
|
-
source
|
|
561
|
+
source,
|
|
528
562
|
})
|
|
529
563
|
.catch(() => {})
|
|
530
564
|
return { trashId: entry.trashId, sessionId: entry.sessionId }
|
package/lib/core/config.js
CHANGED
|
@@ -92,7 +92,10 @@ function readBool(raw, fallback, field, warnings) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function readString(raw, fallback, field, warnings) {
|
|
95
|
-
|
|
95
|
+
// `null` is the documented "unset" spelling for nullable fields
|
|
96
|
+
// (`memory.lingshu.rootDir`, `memory.generic.indexFile`), so it must not be
|
|
97
|
+
// reported as a type error on every activation.
|
|
98
|
+
if (raw === undefined || raw === null) return fallback
|
|
96
99
|
if (typeof raw === 'string') return raw.trim()
|
|
97
100
|
warnings.push(`${field}: expected a string, using ${fallback}`)
|
|
98
101
|
return fallback
|
package/lib/core/errors.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* maps `code` to an HTTP status once, so the UI can branch on the code without
|
|
6
6
|
* parsing prose. Codes are stable API: add new ones, never rename existing ones.
|
|
7
7
|
*/
|
|
8
|
+
import { redactDetails } from './redact.js'
|
|
8
9
|
|
|
9
10
|
/** HTTP status per error code. Keep this table exhaustive for every thrown code. */
|
|
10
11
|
export const ERROR_STATUS = Object.freeze({
|
|
11
12
|
E_DISABLED: 503,
|
|
12
13
|
E_FORBIDDEN_ORIGIN: 403,
|
|
13
14
|
E_FORBIDDEN_HOST: 403,
|
|
15
|
+
E_UNAUTHORIZED: 401,
|
|
14
16
|
E_BAD_REQUEST: 400,
|
|
15
17
|
E_NOT_FOUND: 404,
|
|
16
18
|
E_POLICY_PROTECTED: 403,
|
|
@@ -29,6 +31,7 @@ export const ERROR_STATUS = Object.freeze({
|
|
|
29
31
|
E_TRASH_NOT_FOUND: 404,
|
|
30
32
|
E_TRASH_CONFLICT: 409,
|
|
31
33
|
E_MEMORY_FAILED: 500,
|
|
34
|
+
E_LOCK_BUSY: 409,
|
|
32
35
|
E_INTERNAL: 500,
|
|
33
36
|
})
|
|
34
37
|
|
|
@@ -69,10 +72,10 @@ export function asDeleteError(error, fallbackCode = 'E_INTERNAL') {
|
|
|
69
72
|
}, { cause: error })
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
/** Wire shape for a failed call. */
|
|
75
|
+
/** Wire shape for a failed call. Host paths are redacted before crossing HTTP. */
|
|
73
76
|
export function toWireError(error) {
|
|
74
77
|
const normalized = asDeleteError(error)
|
|
75
|
-
return { ok: false, error: normalized.toJSON() }
|
|
78
|
+
return { ok: false, error: redactDetails(normalized.toJSON()) }
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
/** Wire shape for a successful call. */
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small cross-process advisory lock built from an exclusively-created file.
|
|
3
|
+
*
|
|
4
|
+
* This is NOT a replacement for an OS byte-range lock: it coordinates writers
|
|
5
|
+
* that use the same protocol (this plugin's Host processes and its memory
|
|
6
|
+
* adapter). It deliberately never touches another program's lock file — the
|
|
7
|
+
* 灵枢 (md_cg) store keeps a permanent, zero-byte `<index>.lock` that is
|
|
8
|
+
* guarded by `msvcrt.locking`/`flock`; treating that file's existence as a
|
|
9
|
+
* lock (or deleting it) breaks its writer instead of excluding it.
|
|
10
|
+
*
|
|
11
|
+
* Semantics:
|
|
12
|
+
* - acquisition creates `<lockPath>` with `wx`, writes `{pid, nonce, at}`;
|
|
13
|
+
* - a holder refreshes the file mtime on a heartbeat, so a long operation is
|
|
14
|
+
* never mistaken for a crashed one;
|
|
15
|
+
* - a lock is stale when its mtime is older than `staleMs`, or when the
|
|
16
|
+
* recorded pid is provably gone; a stale lock is removed and retried;
|
|
17
|
+
* - release only removes the file when it still carries this owner's nonce,
|
|
18
|
+
* so a lock broken by a peer cannot make us delete the peer's lock.
|
|
19
|
+
*
|
|
20
|
+
* Every wait sleeps, and the wait budget is checked on every iteration: a
|
|
21
|
+
* held lock can never spin forever (the bug this module replaces).
|
|
22
|
+
*/
|
|
23
|
+
import * as defaultFs from 'node:fs/promises'
|
|
24
|
+
import { randomBytes } from 'node:crypto'
|
|
25
|
+
|
|
26
|
+
const sleepDefault = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
27
|
+
|
|
28
|
+
function defaultIsProcessAlive(pid) {
|
|
29
|
+
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, 0)
|
|
32
|
+
return true
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// EPERM means the process exists but is owned by someone else.
|
|
35
|
+
return error?.code === 'EPERM'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseOwner(raw) {
|
|
40
|
+
try {
|
|
41
|
+
const value = JSON.parse(String(raw))
|
|
42
|
+
if (value !== null && typeof value === 'object') {
|
|
43
|
+
return { pid: Number.isInteger(value.pid) ? value.pid : null, nonce: typeof value.nonce === 'string' ? value.nonce : null }
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// A legacy/foreign lock may be empty or plain text; it has no owner.
|
|
47
|
+
}
|
|
48
|
+
return { pid: null, nonce: null }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {object} options
|
|
53
|
+
* @param {string} options.lockPath absolute path of the lock file
|
|
54
|
+
* @param {number} [options.timeoutMs] wait budget for one acquisition
|
|
55
|
+
* @param {number} [options.staleMs] age after which an ownerless lock is broken
|
|
56
|
+
* @param {number} [options.heartbeatMs] mtime refresh interval while held
|
|
57
|
+
* @param {number} [options.retryMs] poll interval
|
|
58
|
+
* @param {() => number} [options.now]
|
|
59
|
+
* @param {(ms: number) => Promise<void>} [options.sleep]
|
|
60
|
+
* @param {(pid: number) => boolean} [options.isProcessAlive]
|
|
61
|
+
* @param {object} [options.fsApi]
|
|
62
|
+
* @param {number} [options.pid]
|
|
63
|
+
*/
|
|
64
|
+
export function createFileLock({
|
|
65
|
+
lockPath,
|
|
66
|
+
timeoutMs = 15_000,
|
|
67
|
+
staleMs = 120_000,
|
|
68
|
+
heartbeatMs = 5_000,
|
|
69
|
+
retryMs = 50,
|
|
70
|
+
now = Date.now,
|
|
71
|
+
sleep = sleepDefault,
|
|
72
|
+
isProcessAlive = defaultIsProcessAlive,
|
|
73
|
+
fsApi = defaultFs,
|
|
74
|
+
pid = process.pid,
|
|
75
|
+
}) {
|
|
76
|
+
if (typeof lockPath !== 'string' || lockPath === '') throw new Error('file-lock: lockPath is required')
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Acquire the lock.
|
|
80
|
+
* @returns {Promise<() => Promise<void>>} release
|
|
81
|
+
*/
|
|
82
|
+
async function acquire() {
|
|
83
|
+
const nonce = randomBytes(8).toString('hex')
|
|
84
|
+
const started = now()
|
|
85
|
+
let handle = null
|
|
86
|
+
|
|
87
|
+
while (handle === null) {
|
|
88
|
+
try {
|
|
89
|
+
handle = await fsApi.open(lockPath, 'wx')
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error?.code !== 'EEXIST') throw error
|
|
92
|
+
let info
|
|
93
|
+
try {
|
|
94
|
+
info = await fsApi.stat(lockPath)
|
|
95
|
+
} catch (statError) {
|
|
96
|
+
if (statError?.code === 'ENOENT') continue
|
|
97
|
+
throw statError
|
|
98
|
+
}
|
|
99
|
+
const age = Math.max(0, now() - info.mtimeMs)
|
|
100
|
+
let owner = { pid: null, nonce: null }
|
|
101
|
+
try {
|
|
102
|
+
owner = parseOwner(await fsApi.readFile(lockPath, 'utf8'))
|
|
103
|
+
} catch {
|
|
104
|
+
// Unreadable owner text is treated as ownerless, not as a crash.
|
|
105
|
+
}
|
|
106
|
+
const deadOwner = owner.pid !== null && !isProcessAlive(owner.pid)
|
|
107
|
+
if (age > staleMs || deadOwner) {
|
|
108
|
+
await fsApi.rm(lockPath, { force: true }).catch(() => {})
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
if (now() - started >= timeoutMs) {
|
|
112
|
+
const busy = new Error(`The lock ${lockPath} is held by another process.`)
|
|
113
|
+
busy.code = 'E_LOCK_BUSY'
|
|
114
|
+
throw busy
|
|
115
|
+
}
|
|
116
|
+
await sleep(retryMs)
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await handle.writeFile(`${JSON.stringify({ pid, nonce, at: now() })}\n`, 'utf8')
|
|
122
|
+
} catch (error) {
|
|
123
|
+
await handle.close().catch(() => {})
|
|
124
|
+
await fsApi.rm(lockPath, { force: true }).catch(() => {})
|
|
125
|
+
throw error
|
|
126
|
+
}
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const heartbeat = setInterval(() => {
|
|
131
|
+
const stamp = new Date()
|
|
132
|
+
fsApi.utimes(lockPath, stamp, stamp).catch(() => {})
|
|
133
|
+
}, Math.max(250, heartbeatMs))
|
|
134
|
+
if (typeof heartbeat.unref === 'function') heartbeat.unref()
|
|
135
|
+
|
|
136
|
+
let released = false
|
|
137
|
+
return async () => {
|
|
138
|
+
if (released) return
|
|
139
|
+
released = true
|
|
140
|
+
clearInterval(heartbeat)
|
|
141
|
+
try {
|
|
142
|
+
const owner = parseOwner(await fsApi.readFile(lockPath, 'utf8').catch(() => ''))
|
|
143
|
+
if (owner.nonce === nonce) await fsApi.rm(lockPath, { force: true }).catch(() => {})
|
|
144
|
+
} finally {
|
|
145
|
+
await handle?.close().catch(() => {})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { acquire, lockPath }
|
|
151
|
+
}
|
|
@@ -17,13 +17,13 @@ export function createGenericJsonAdapter({ indexFile, rootDir, sessionField = 's
|
|
|
17
17
|
if (typeof indexFile !== 'string' || indexFile === '') return null
|
|
18
18
|
return createJsonIndexMemoryAdapter({
|
|
19
19
|
id: 'generic-json',
|
|
20
|
-
label:
|
|
20
|
+
// Neutral label: the Client localizes by adapter id.
|
|
21
|
+
label: 'generic-json',
|
|
21
22
|
indexFile,
|
|
22
23
|
rootDir: typeof rootDir === 'string' && rootDir !== '' ? rootDir : null,
|
|
23
24
|
sessionField,
|
|
24
25
|
nodesPointer,
|
|
25
26
|
contentField: rootDir === null ? null : 'path',
|
|
26
|
-
useStoreLock: false,
|
|
27
27
|
now,
|
|
28
28
|
fsApi,
|
|
29
29
|
})
|