dsh-vscode-mode 0.25.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -11
- package/lib/client.js +1969 -1344
- package/lib/client.js.map +2 -2
- package/lib/index.js +1443 -716
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compatSummary.ts +4 -0
- package/src/client/contextMenusOfficial.ts +80 -0
- package/src/client/forgeBridge.ts +28 -0
- package/src/client/index.ts +18 -3
- package/src/client/monaco/lsp/lspClient.ts +25 -7
- package/src/client/officialSidebar.ts +15 -3
- package/src/client/reviewBar.ts +3 -0
- package/src/client/reviewBatch.ts +9 -1
- package/src/client/reviewKeepAll.ts +72 -0
- package/src/client/sidebar/menuItems.ts +6 -0
- package/src/client/sidebar/panels/FileExplorer.ts +26 -1
- package/src/client/sidebar/types.ts +3 -0
- package/src/client/state/diffSummary.ts +34 -7
- package/src/client/state/recordsFeed.ts +40 -3
- package/src/client/state/reviewFeed.ts +9 -2
- package/src/client/tabMenu.ts +5 -0
- package/src/client/ui/ClaimRouter.ts +11 -5
- package/src/client/ui/EditorPane.ts +9 -1
- package/src/client/ui/EditorView.ts +164 -39
- package/src/client/ui/OfficialSideTab.ts +13 -5
- package/src/client/ui/ReviewAllConfirm.ts +1 -1
- package/src/index.ts +1 -1
- package/src/pending/capture.ts +96 -47
- package/src/pending/persist.ts +94 -5
- package/src/pending/store.ts +171 -58
- package/src/pendingCompat.ts +72 -9
- package/src/pendingRpc.ts +10 -10
- package/src/reviewCapture.ts +64 -23
- package/src/reviewDb.ts +119 -8
- package/src/reviewEvidence.ts +1 -1
- package/src/reviewRpc.ts +115 -9
- package/src/rpc.ts +14 -10
- package/src/shared/review.ts +21 -0
- package/src/shared/rpc.ts +3 -1
- package/src/store.ts +44 -0
- package/src/toolTargets.ts +63 -0
package/src/pending/capture.ts
CHANGED
|
@@ -2,15 +2,32 @@
|
|
|
2
2
|
* 待审差异采集:环绕一次文件写入读取真实前后内容,只有内容确实变化才落库。
|
|
3
3
|
* @author ddj 2026年10月29号
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { resolveTarget } from '../store.js'
|
|
5
|
+
import { cwdOfSession } from '../store.js'
|
|
7
6
|
import type { Ctx } from '../store.js'
|
|
8
7
|
import { log } from '../log.js'
|
|
8
|
+
import { toolTargets, patchDiagnostics } from '../toolTargets.js'
|
|
9
|
+
import { rawText, rawReviewFile } from '../reviewEvidence.js'
|
|
9
10
|
|
|
10
11
|
/** 可被采信为待审来源的写文件工具。 */
|
|
11
|
-
const WRITE_TOOLS = ['edit', 'write', 'str_replace_editor']
|
|
12
|
+
const WRITE_TOOLS = ['edit', 'write', 'str_replace_editor', 'apply_patch']
|
|
12
13
|
/** 单文件文本上限:超过即标注 oversized,不静默截断。 */
|
|
13
14
|
const MAX_TEXT = 1024 * 1024
|
|
15
|
+
const captureGaps = new Map<string, { cwd: string; sessionId: string; message: string }>()
|
|
16
|
+
|
|
17
|
+
/** @public @author ddj 2026年11月04号 @param cwd Workspace identity. @returns Bounded runtime-only capture diagnostics. */
|
|
18
|
+
export function pendingGaps(cwd: string): string[] {
|
|
19
|
+
return [...captureGaps.values()].filter((gap) => gap.cwd === cwd).map((gap) => gap.message)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @private @author ddj 2026年11月04号 @param ctx Host scope. @param exec Original call. @param path Failed target. @param error Evidence failure. Records no writable state. */
|
|
23
|
+
function recordGap(scope: { cwd: string; sessionId: string }, exec: WriteExec, path: string, error: unknown): void {
|
|
24
|
+
const { cwd, sessionId } = scope
|
|
25
|
+
const key = JSON.stringify([cwd, sessionId, exec.id, path])
|
|
26
|
+
captureGaps.set(key, { cwd, sessionId, message: path + ': ' + String(error) })
|
|
27
|
+
const local = [...captureGaps].filter(([, gap]) => gap.cwd === cwd)
|
|
28
|
+
while (local.length > 50) captureGaps.delete(local.shift()![0])
|
|
29
|
+
while (captureGaps.size > 500) captureGaps.delete(captureGaps.keys().next().value!)
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
/** 一次已确认的写入。 */
|
|
16
33
|
export interface PendingWrite {
|
|
@@ -33,12 +50,14 @@ export interface PendingCapture {
|
|
|
33
50
|
interface CaptureState {
|
|
34
51
|
busy: Set<string>
|
|
35
52
|
stopped: boolean
|
|
53
|
+
disposed: Set<string>
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
/** 工具的写工具参数形状;只取本采集关心的字段。 */
|
|
39
57
|
interface WriteExec {
|
|
58
|
+
id?: string
|
|
40
59
|
name?: string
|
|
41
|
-
arguments?: { file_path?: unknown; path?: unknown }
|
|
60
|
+
arguments?: { file_path?: unknown; path?: unknown; input?: unknown }
|
|
42
61
|
agent?: { session?: { id?: string; cwd?: string } }
|
|
43
62
|
}
|
|
44
63
|
|
|
@@ -50,12 +69,19 @@ interface WriteExec {
|
|
|
50
69
|
* @returns 可释放的采集器
|
|
51
70
|
*/
|
|
52
71
|
export function installPending(ctx: Ctx, sink: (write: PendingWrite) => void): PendingCapture {
|
|
53
|
-
const state: CaptureState = { busy: new Set<string>(), stopped: false }
|
|
72
|
+
const state: CaptureState = { busy: new Set<string>(), stopped: false, disposed: new Set<string>() }
|
|
54
73
|
ctx.on('tools/execute', (exec: WriteExec, next: () => Promise<unknown>) => aroundTool(ctx, state, sink, exec, next))
|
|
74
|
+
ctx.on('session/disposed', (session: { id: string }) => {
|
|
75
|
+
state.disposed.add(session.id)
|
|
76
|
+
for (const [key, gap] of captureGaps) {
|
|
77
|
+
if (gap.sessionId === session.id) captureGaps.delete(key)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
55
80
|
return {
|
|
56
81
|
dispose(): void {
|
|
57
82
|
state.stopped = true
|
|
58
83
|
state.busy.clear()
|
|
84
|
+
captureGaps.clear()
|
|
59
85
|
},
|
|
60
86
|
}
|
|
61
87
|
}
|
|
@@ -68,20 +94,24 @@ export function installPending(ctx: Ctx, sink: (write: PendingWrite) => void): P
|
|
|
68
94
|
* @param exec 工具分发上下文
|
|
69
95
|
* @returns 采集计划或 null
|
|
70
96
|
*/
|
|
71
|
-
async function planOf(
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!WRITE_TOOLS.includes(String(exec.name))) return null
|
|
76
|
-
const cwd = cwdOf(session as never)
|
|
77
|
-
if (!cwd) return null
|
|
78
|
-
const fs = ctx.get('fs')
|
|
79
|
-
const target = await resolveTarget(ctx, session as never, requested)
|
|
97
|
+
async function planOf(scope: { cwd: string; sessionId: string; fs: any }, state: CaptureState, requested: string): Promise<CapturePlan | null> {
|
|
98
|
+
const { cwd, sessionId, fs } = scope
|
|
99
|
+
if (state.stopped || state.disposed.has(sessionId)) return null
|
|
100
|
+
const target = await fs.resolve(requested, { cwd })
|
|
80
101
|
const identity = fs.processPath(target)
|
|
81
|
-
if (typeof identity !== 'string' || !identity || state.busy.has(identity)) return null
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
if (state.stopped || state.disposed.has(sessionId) || typeof identity !== 'string' || !identity || state.busy.has(identity)) return null
|
|
103
|
+
state.busy.add(identity)
|
|
104
|
+
try {
|
|
105
|
+
const before = await readText(fs, target)
|
|
106
|
+
if (state.stopped || state.disposed.has(sessionId) || (before !== null && before.length > MAX_TEXT)) {
|
|
107
|
+
state.busy.delete(identity)
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
return { cwd: String(cwd), sessionId, path: requested, identity, before, target, fs }
|
|
111
|
+
} catch (error) {
|
|
112
|
+
state.busy.delete(identity)
|
|
113
|
+
throw error
|
|
114
|
+
}
|
|
85
115
|
}
|
|
86
116
|
|
|
87
117
|
/** 一次采集所需的固定信息。 */
|
|
@@ -91,7 +121,8 @@ interface CapturePlan {
|
|
|
91
121
|
path: string
|
|
92
122
|
identity: string
|
|
93
123
|
before: string | null
|
|
94
|
-
|
|
124
|
+
target: unknown
|
|
125
|
+
fs: any
|
|
95
126
|
}
|
|
96
127
|
|
|
97
128
|
/**
|
|
@@ -108,58 +139,76 @@ async function aroundTool(
|
|
|
108
139
|
ctx: Ctx, state: CaptureState, sink: (write: PendingWrite) => void,
|
|
109
140
|
exec: WriteExec, next: () => Promise<unknown>,
|
|
110
141
|
): Promise<unknown> {
|
|
111
|
-
|
|
142
|
+
if (state.stopped || !WRITE_TOOLS.includes(String(exec.name))) return next()
|
|
143
|
+
const session = exec.agent?.session
|
|
144
|
+
const cwd = session ? cwdOfSession(ctx, session as never) : null
|
|
145
|
+
if (!session?.id || !cwd) return next()
|
|
146
|
+
const scope = { cwd, sessionId: session.id, fs: ctx.get('fs') }
|
|
147
|
+
const plans: CapturePlan[] = []
|
|
112
148
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
149
|
+
const paths = await toolTargets(exec)
|
|
150
|
+
for (const path of paths) {
|
|
151
|
+
try {
|
|
152
|
+
if (plans.length >= 16) throw new Error('capture target budget exceeded')
|
|
153
|
+
const plan = await planOf(scope, state, path)
|
|
154
|
+
if (plan) plans.push(plan)
|
|
155
|
+
} catch (error) { if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, path, error); log.debug('待审采集跳过:' + path + ': ' + String(error)) }
|
|
156
|
+
}
|
|
157
|
+
} catch (error) { if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, String(exec.name), error); log.debug('待审目标不可用:' + String(error)) }
|
|
118
158
|
|
|
119
|
-
state.busy.add(plan.identity)
|
|
120
159
|
try {
|
|
121
|
-
|
|
160
|
+
const result = await next()
|
|
161
|
+
if (exec.name === 'apply_patch' && !state.stopped && !state.disposed.has(scope.sessionId)) {
|
|
162
|
+
for (const diagnostic of patchDiagnostics(result)) recordGap(scope, exec, 'apply_patch', diagnostic)
|
|
163
|
+
}
|
|
164
|
+
return result
|
|
122
165
|
} finally {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
log.debug('待审上报失败:' + String(error))
|
|
166
|
+
for (const plan of plans) {
|
|
167
|
+
try { await reportWrite(state, sink, plan) }
|
|
168
|
+
catch (error) { if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, plan.path, error); log.debug('待审上报失败:' + String(error)) }
|
|
169
|
+
finally { state.busy.delete(plan.identity) }
|
|
128
170
|
}
|
|
129
171
|
}
|
|
130
172
|
}
|
|
131
173
|
|
|
132
174
|
/**
|
|
133
|
-
* @author ddj 2026年
|
|
134
|
-
* @description:
|
|
135
|
-
* @param
|
|
175
|
+
* @author ddj 2026年11月04号
|
|
176
|
+
* @description: 固定目标写后读回;内容一致不产生条目,卸载后不再上报。
|
|
177
|
+
* @param state 采集生命周期状态
|
|
136
178
|
* @param sink 已确认写入的接收方
|
|
137
179
|
* @param plan 采集计划
|
|
138
180
|
* @returns 无;读回失败只记日志
|
|
139
181
|
*/
|
|
140
|
-
function reportWrite(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
182
|
+
async function reportWrite(state: CaptureState, sink: (write: PendingWrite) => void, plan: CapturePlan): Promise<void> {
|
|
183
|
+
if (state.stopped || state.disposed.has(plan.sessionId)) return
|
|
184
|
+
// 目录、provider 和目标均在写前冻结,切目录或卸载不能将内容串到其他工作区。
|
|
185
|
+
const after = await readText(plan.fs, plan.target)
|
|
186
|
+
if (state.stopped || state.disposed.has(plan.sessionId)) return
|
|
187
|
+
if (after !== null && after.length > MAX_TEXT) throw new Error('capture text budget exceeded')
|
|
188
|
+
for (const [key, gap] of captureGaps) {
|
|
189
|
+
const identity = JSON.parse(key) as unknown[]
|
|
190
|
+
if (gap.cwd === plan.cwd && gap.sessionId === plan.sessionId && identity[3] === plan.path) captureGaps.delete(key)
|
|
191
|
+
}
|
|
192
|
+
if (plan.before === after) return
|
|
193
|
+
sink({ cwd: plan.cwd, sessionId: plan.sessionId, path: plan.path, identity: plan.identity, before: plan.before, after, now: Date.now() })
|
|
148
194
|
}
|
|
149
195
|
|
|
150
196
|
/**
|
|
151
197
|
* @author ddj 2026年10月29号
|
|
152
|
-
* @description:
|
|
198
|
+
* @description: 读取全文;只有明确缺失返回 null,不可读或超限错误保留失败语义。
|
|
153
199
|
* @param fs 文件系统
|
|
154
200
|
* @param target 目标
|
|
155
201
|
* @returns 文本或 null
|
|
156
202
|
*/
|
|
157
|
-
export async function readText(fs: any, target: any): Promise<string | null> {
|
|
203
|
+
export async function readText(fs: any, target: any, signal?: AbortSignal): Promise<string | null> {
|
|
158
204
|
try {
|
|
159
|
-
|
|
205
|
+
if (typeof fs.readBytes === 'function') {
|
|
206
|
+
return rawText(rawReviewFile(await fs.readBytes(target, signal, MAX_TEXT)))
|
|
207
|
+
}
|
|
208
|
+
throw new Error('Raw byte capture unavailable')
|
|
160
209
|
} catch (error) {
|
|
161
210
|
const code = (error as { code?: string }).code
|
|
162
|
-
if (code === 'ENOENT' || code === 'FS_NOT_FOUND'
|
|
211
|
+
if (code === 'ENOENT' || code === 'FS_NOT_FOUND') return null
|
|
163
212
|
throw error
|
|
164
213
|
}
|
|
165
214
|
}
|
package/src/pending/persist.ts
CHANGED
|
@@ -107,8 +107,20 @@ export function loadEntries(cwd: string): StoredEntry[] {
|
|
|
107
107
|
* @returns 无
|
|
108
108
|
*/
|
|
109
109
|
export function saveEntry(cwd: string, stored: StoredEntry): void {
|
|
110
|
+
upsertEntry(pendingDb(cwd), stored)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @private
|
|
115
|
+
* @author ddj 2026年11月03号
|
|
116
|
+
* @description: 在给定连接上写入或更新一条条目(含标记),供单条与批量两个入口复用。
|
|
117
|
+
* @param db 已迁移连接(可能已在事务中)
|
|
118
|
+
* @param stored 条目与标记
|
|
119
|
+
* @returns 无
|
|
120
|
+
*/
|
|
121
|
+
function upsertEntry(db: DatabaseSync, stored: StoredEntry): void {
|
|
110
122
|
const { entry, flags } = stored
|
|
111
|
-
|
|
123
|
+
db.prepare(`INSERT INTO pending_entries
|
|
112
124
|
(id,path,earlier,old_text,new_text,updated_at,unseen,missing,diverged,oversized,revision,session_id,session_ids,lineage)
|
|
113
125
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
114
126
|
ON CONFLICT(id) DO UPDATE SET path=excluded.path,earlier=excluded.earlier,old_text=excluded.old_text,
|
|
@@ -165,9 +177,7 @@ export function saveBlocks(cwd: string, entryId: string, blocks: { id: string; d
|
|
|
165
177
|
const db = pendingDb(cwd)
|
|
166
178
|
db.exec('BEGIN IMMEDIATE')
|
|
167
179
|
try {
|
|
168
|
-
db
|
|
169
|
-
const insert = db.prepare('INSERT INTO pending_blocks VALUES(?,?,?)')
|
|
170
|
-
for (const block of blocks) insert.run(entryId, block.id, block.decision)
|
|
180
|
+
writeBlocks(db, entryId, blocks)
|
|
171
181
|
db.exec('COMMIT')
|
|
172
182
|
} catch (error) {
|
|
173
183
|
if (db.isTransaction) db.exec('ROLLBACK')
|
|
@@ -175,6 +185,21 @@ export function saveBlocks(cwd: string, entryId: string, blocks: { id: string; d
|
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
187
|
|
|
188
|
+
/**
|
|
189
|
+
* @private
|
|
190
|
+
* @author ddj 2026年11月03号
|
|
191
|
+
* @description: 在给定连接上覆盖某条目的块决定集合,供单条与批量两个入口复用。
|
|
192
|
+
* @param db 已迁移连接(可能已在事务中)
|
|
193
|
+
* @param entryId 条目身份
|
|
194
|
+
* @param blocks 块决定
|
|
195
|
+
* @returns 无
|
|
196
|
+
*/
|
|
197
|
+
function writeBlocks(db: DatabaseSync, entryId: string, blocks: { id: string; decision: string }[]): void {
|
|
198
|
+
db.prepare('DELETE FROM pending_blocks WHERE entry_id=?').run(entryId)
|
|
199
|
+
const insert = db.prepare('INSERT INTO pending_blocks VALUES(?,?,?)')
|
|
200
|
+
for (const block of blocks) insert.run(entryId, block.id, block.decision)
|
|
201
|
+
}
|
|
202
|
+
|
|
178
203
|
|
|
179
204
|
/**
|
|
180
205
|
* @author ddj 2026年10月29号
|
|
@@ -222,7 +247,21 @@ export function writeOp(
|
|
|
222
247
|
* @returns 无
|
|
223
248
|
*/
|
|
224
249
|
export function pushHistory(cwd: string, kind: string, entryId: string, snapshot: string, now: number): void {
|
|
225
|
-
|
|
250
|
+
insertHistory(pendingDb(cwd), kind, entryId, snapshot, now)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @private
|
|
255
|
+
* @author ddj 2026年11月03号
|
|
256
|
+
* @description: 在给定连接上追加撤销栈记录并截断重做分支,供单条与批量两个入口复用。
|
|
257
|
+
* @param db 已迁移连接(可能已在事务中)
|
|
258
|
+
* @param kind 操作种类
|
|
259
|
+
* @param entryId 涉及的条目
|
|
260
|
+
* @param snapshot 可重放快照
|
|
261
|
+
* @param now 时间
|
|
262
|
+
* @returns 无
|
|
263
|
+
*/
|
|
264
|
+
function insertHistory(db: DatabaseSync, kind: string, entryId: string, snapshot: string, now: number): void {
|
|
226
265
|
db.prepare("DELETE FROM pending_history WHERE state='undone'").run()
|
|
227
266
|
db.prepare("INSERT INTO pending_history(kind,entry_id,snapshot,created_at,state) VALUES(?,?,?,?,'applied')")
|
|
228
267
|
.run(kind, entryId, snapshot, now)
|
|
@@ -327,3 +366,53 @@ function flagsOfRow(row: Row): PendingFlags {
|
|
|
327
366
|
revision: Number(row.revision),
|
|
328
367
|
}
|
|
329
368
|
}
|
|
369
|
+
|
|
370
|
+
/** 批量 Keep 的一条落库记录:stored 提供 CAS 依据与撤销快照 before,next 是推进后的条目。 */
|
|
371
|
+
export interface KeepRecord {
|
|
372
|
+
entryId: string
|
|
373
|
+
stored: StoredEntry
|
|
374
|
+
next: StoredEntry
|
|
375
|
+
blocks: { id: string; decision: string }[]
|
|
376
|
+
snapshot: string
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* @public
|
|
381
|
+
* @author ddj 2026年11月03号
|
|
382
|
+
* @description: 在一次事务内批量推进多条待审条目(批量 Keep 用)。每条按 revision 做 CAS,
|
|
383
|
+
* 只跳过冲突的那一条,不牵连整批;块决定与撤销快照与单条路径同口径。
|
|
384
|
+
* @param cwd 工作区
|
|
385
|
+
* @param records 待落库记录
|
|
386
|
+
* @returns 因版本已变被跳过的条目
|
|
387
|
+
* @throws 事务级异常(SQL/程序错误)整体回滚后抛出
|
|
388
|
+
*/
|
|
389
|
+
export function saveDecisions(cwd: string, records: KeepRecord[]): { id: string; reason: string }[] {
|
|
390
|
+
const db = pendingDb(cwd)
|
|
391
|
+
const skipped: { id: string; reason: string }[] = []
|
|
392
|
+
const advance = db.prepare(`UPDATE pending_entries SET path=?,earlier=?,old_text=?,new_text=?,updated_at=?,unseen=?,
|
|
393
|
+
missing=?,diverged=?,oversized=?,revision=?,session_id=?,session_ids=?,lineage=? WHERE id=? AND revision=?`)
|
|
394
|
+
|
|
395
|
+
db.exec('BEGIN IMMEDIATE')
|
|
396
|
+
try {
|
|
397
|
+
for (const record of records) {
|
|
398
|
+
const { entry, flags } = record.next
|
|
399
|
+
const expected = record.stored.flags.revision
|
|
400
|
+
const changed = advance.run(entry.path, entry.earlierVersion, entry.oldText, entry.newText, entry.updatedAt,
|
|
401
|
+
entry.unseen === true ? 1 : 0, flags.missing ? 1 : 0, flags.diverged ? 1 : 0, flags.oversized ? 1 : 0,
|
|
402
|
+
flags.revision, entry.sessionId, JSON.stringify(entry.sessionIds),
|
|
403
|
+
entry.lineage ? JSON.stringify(entry.lineage) : null, record.entryId, expected)
|
|
404
|
+
// 读条目到落库之间被别的决定推进过:只放弃这一条
|
|
405
|
+
if (changed.changes !== 1) {
|
|
406
|
+
skipped.push({ id: record.entryId, reason: '待审条目已变化,请重试' })
|
|
407
|
+
continue
|
|
408
|
+
}
|
|
409
|
+
writeBlocks(db, record.entryId, record.blocks)
|
|
410
|
+
insertHistory(db, 'decide', record.entryId, record.snapshot, Date.now())
|
|
411
|
+
}
|
|
412
|
+
db.exec('COMMIT')
|
|
413
|
+
return skipped
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (db.isTransaction) db.exec('ROLLBACK')
|
|
416
|
+
throw error
|
|
417
|
+
}
|
|
418
|
+
}
|