@sema-agent/server 1.315.0 → 1.316.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/dist/http/idempotency.d.ts +1 -0
- package/dist/http/idempotency.js +3 -0
- package/dist/http/principal-gate.d.ts +12 -0
- package/dist/http/principal-gate.js +52 -0
- package/dist/http/route-ctx.d.ts +67 -0
- package/dist/http/route-ctx.js +2 -0
- package/dist/http/routes/attachments.d.ts +4 -0
- package/dist/http/routes/attachments.js +106 -0
- package/dist/http/routes/capabilities.d.ts +4 -0
- package/dist/http/routes/capabilities.js +111 -0
- package/dist/http/routes/images.d.ts +20 -0
- package/dist/http/routes/images.js +572 -0
- package/dist/http/routes/memory-policy.d.ts +4 -0
- package/dist/http/routes/memory-policy.js +105 -0
- package/dist/http/routes/observability.d.ts +4 -0
- package/dist/http/routes/observability.js +63 -0
- package/dist/http/routes/session-sync.d.ts +36 -0
- package/dist/http/routes/session-sync.js +513 -0
- package/dist/http/routes/sessions-list.d.ts +4 -0
- package/dist/http/routes/sessions-list.js +117 -0
- package/dist/http/routes/sessions.d.ts +17 -0
- package/dist/http/routes/sessions.js +746 -0
- package/dist/http/send.d.ts +6 -0
- package/dist/http/send.js +19 -0
- package/dist/http/server.d.ts +6 -7
- package/dist/http/server.js +125 -2413
- package/dist/http/sse-log.d.ts +25 -0
- package/dist/http/sse-log.js +59 -0
- package/dist/http/workspace-content.d.ts +3 -0
- package/dist/http/workspace-content.js +20 -0
- package/dist/plugins/approval-store-sql.d.ts +52 -0
- package/dist/plugins/approval-store-sql.js +119 -0
- package/dist/plugins/breaker-state-sql.d.ts +29 -0
- package/dist/plugins/breaker-state-sql.js +104 -0
- package/dist/plugins/file-snapshot-store-sql.d.ts +38 -0
- package/dist/plugins/file-snapshot-store-sql.js +210 -0
- package/dist/plugins/image-index-sql.d.ts +33 -0
- package/dist/plugins/image-index-sql.js +328 -0
- package/dist/plugins/outcome-ledger-sql.d.ts +70 -0
- package/dist/plugins/outcome-ledger-sql.js +158 -0
- package/dist/plugins/pg-approval-store.d.ts +1 -27
- package/dist/plugins/pg-approval-store.js +1 -83
- package/dist/plugins/pg-breaker-state.d.ts +1 -19
- package/dist/plugins/pg-breaker-state.js +1 -88
- package/dist/plugins/pg-file-snapshot-store.d.ts +1 -27
- package/dist/plugins/pg-file-snapshot-store.js +1 -190
- package/dist/plugins/pg-image-index.d.ts +1 -21
- package/dist/plugins/pg-image-index.js +1 -217
- package/dist/plugins/pg-outcome-ledger.d.ts +2 -22
- package/dist/plugins/pg-outcome-ledger.js +1 -104
- package/dist/plugins/pg-resume-anchor-store.d.ts +1 -13
- package/dist/plugins/pg-resume-anchor-store.js +1 -22
- package/dist/plugins/pg-run-store.d.ts +1 -75
- package/dist/plugins/pg-run-store.js +1 -287
- package/dist/plugins/pg-session-policy-store.d.ts +1 -11
- package/dist/plugins/pg-session-policy-store.js +1 -90
- package/dist/plugins/pg-tool-result-store.d.ts +1 -15
- package/dist/plugins/pg-tool-result-store.js +1 -52
- package/dist/plugins/pg-workflow-journal-store.d.ts +1 -20
- package/dist/plugins/pg-workflow-journal-store.js +1 -34
- package/dist/plugins/pg-workflow-run-store.d.ts +1 -55
- package/dist/plugins/pg-workflow-run-store.js +1 -220
- package/dist/plugins/resume-anchor-store-sql.d.ts +23 -0
- package/dist/plugins/resume-anchor-store-sql.js +38 -0
- package/dist/plugins/run-store-sql.d.ts +89 -0
- package/dist/plugins/run-store-sql.js +397 -0
- package/dist/plugins/session-policy-store-sql.d.ts +22 -0
- package/dist/plugins/session-policy-store-sql.js +109 -0
- package/dist/plugins/tidb-approval-store.d.ts +1 -39
- package/dist/plugins/tidb-approval-store.js +1 -71
- package/dist/plugins/tidb-breaker-state.d.ts +1 -17
- package/dist/plugins/tidb-breaker-state.js +1 -74
- package/dist/plugins/tidb-file-snapshot-store.d.ts +1 -27
- package/dist/plugins/tidb-file-snapshot-store.js +1 -192
- package/dist/plugins/tidb-image-index.d.ts +1 -19
- package/dist/plugins/tidb-image-index.js +1 -149
- package/dist/plugins/tidb-outcome-ledger.d.ts +1 -57
- package/dist/plugins/tidb-outcome-ledger.js +1 -97
- package/dist/plugins/tidb-resume-anchor-store.d.ts +1 -13
- package/dist/plugins/tidb-resume-anchor-store.js +1 -22
- package/dist/plugins/tidb-run-store.d.ts +1 -75
- package/dist/plugins/tidb-run-store.js +1 -306
- package/dist/plugins/tidb-session-policy-store.d.ts +1 -11
- package/dist/plugins/tidb-session-policy-store.js +1 -86
- package/dist/plugins/tidb-tool-result-store.d.ts +1 -13
- package/dist/plugins/tidb-tool-result-store.js +1 -38
- package/dist/plugins/tidb-workflow-journal-store.d.ts +1 -20
- package/dist/plugins/tidb-workflow-journal-store.js +1 -34
- package/dist/plugins/tidb-workflow-run-store.d.ts +1 -61
- package/dist/plugins/tidb-workflow-run-store.js +1 -231
- package/dist/plugins/tool-result-store-sql.d.ts +25 -0
- package/dist/plugins/tool-result-store-sql.js +69 -0
- package/dist/plugins/workflow-journal-store-sql.d.ts +30 -0
- package/dist/plugins/workflow-journal-store-sql.js +53 -0
- package/dist/plugins/workflow-run-store-sql.d.ts +85 -0
- package/dist/plugins/workflow-run-store-sql.js +299 -0
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
export interface SseLogProvider<E extends {
|
|
3
|
+
seq: number;
|
|
4
|
+
}> {
|
|
5
|
+
statusOf(id: string): Promise<{
|
|
6
|
+
status: string;
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
} | null | undefined>;
|
|
9
|
+
getEvents(id: string, after: number): Promise<E[]>;
|
|
10
|
+
retainedFrom(id: string): Promise<number>;
|
|
11
|
+
formatEvent(ev: E): {
|
|
12
|
+
id: number;
|
|
13
|
+
event: string;
|
|
14
|
+
data: unknown;
|
|
15
|
+
};
|
|
16
|
+
staleFrame(): {
|
|
17
|
+
event: string;
|
|
18
|
+
data: unknown;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare function streamSseLog<E extends {
|
|
22
|
+
seq: number;
|
|
23
|
+
}>(req: IncomingMessage, res: ServerResponse, provider: SseLogProvider<E>, id: string, staleMs: number): Promise<void>;
|
|
24
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
25
|
+
//# sourceMappingURL=sse-log.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { sendJson, sseHeaders } from "./send.js";
|
|
2
|
+
export async function streamSseLog(req, res, provider, id, staleMs) {
|
|
3
|
+
const lastId = Number(req.headers["last-event-id"] ?? new URL(req.url ?? "", "http://x").searchParams.get("from") ?? 0);
|
|
4
|
+
let from = Number.isFinite(lastId) ? lastId : 0;
|
|
5
|
+
if (from > 0) {
|
|
6
|
+
const retainedFrom = await provider.retainedFrom(id);
|
|
7
|
+
if (retainedFrom > from + 1) {
|
|
8
|
+
sendJson(res, 416, { error: "resume point evicted past retention", retainedFrom });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
sseHeaders(res);
|
|
13
|
+
let closed = false;
|
|
14
|
+
req.on("close", () => {
|
|
15
|
+
closed = true;
|
|
16
|
+
});
|
|
17
|
+
const start = Date.now();
|
|
18
|
+
const MAX_MS = 15 * 60 * 1000;
|
|
19
|
+
let lastBeat = Date.now();
|
|
20
|
+
const write = (events) => {
|
|
21
|
+
for (const ev of events) {
|
|
22
|
+
const f = provider.formatEvent(ev);
|
|
23
|
+
res.write(`id: ${f.id}\nevent: ${f.event}\ndata: ${JSON.stringify(f.data)}\n\n`);
|
|
24
|
+
from = ev.seq;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
while (!closed) {
|
|
28
|
+
const [events, row] = await Promise.all([provider.getEvents(id, from), provider.statusOf(id)]);
|
|
29
|
+
write(events);
|
|
30
|
+
if (!row)
|
|
31
|
+
break;
|
|
32
|
+
if (row.status !== "running") {
|
|
33
|
+
write(await provider.getEvents(id, from));
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
const stale = Date.now() - new Date(row.updatedAt).getTime() > staleMs;
|
|
37
|
+
if (stale && events.length === 0) {
|
|
38
|
+
const sf = provider.staleFrame();
|
|
39
|
+
res.write(`event: ${sf.event}\ndata: ${JSON.stringify(sf.data)}\n\n`);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (Date.now() - start > MAX_MS) {
|
|
43
|
+
res.write(`event: error\ndata: ${JSON.stringify({ type: "error", code: "STREAM_MAX_DURATION", message: "stream reached its 15-minute cap — reconnect with Last-Event-ID to continue (the run is still active)" })}\n\n`);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (events.length === 0) {
|
|
47
|
+
if (Date.now() - lastBeat > 15_000) {
|
|
48
|
+
res.write(`event: heartbeat\ndata: {}\n\n`);
|
|
49
|
+
lastBeat = Date.now();
|
|
50
|
+
}
|
|
51
|
+
await sleep(250);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
res.end();
|
|
55
|
+
}
|
|
56
|
+
export function sleep(ms) {
|
|
57
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=sse-log.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function workspaceContentType(relPath) {
|
|
2
|
+
const base = relPath.slice(relPath.lastIndexOf("/") + 1);
|
|
3
|
+
const dot = base.lastIndexOf(".");
|
|
4
|
+
const ext = dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
|
|
5
|
+
const TABLE = {
|
|
6
|
+
txt: "text/plain; charset=utf-8", md: "text/plain; charset=utf-8", log: "text/plain; charset=utf-8",
|
|
7
|
+
json: "application/json; charset=utf-8", csv: "text/csv; charset=utf-8",
|
|
8
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp",
|
|
9
|
+
pdf: "application/pdf",
|
|
10
|
+
};
|
|
11
|
+
return TABLE[ext] ?? "application/octet-stream";
|
|
12
|
+
}
|
|
13
|
+
export function looksBinary(bytes) {
|
|
14
|
+
const n = Math.min(bytes.byteLength, 8192);
|
|
15
|
+
for (let i = 0; i < n; i++)
|
|
16
|
+
if (bytes[i] === 0)
|
|
17
|
+
return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=workspace-content.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
|
+
import type { Pool as PgPool, PoolClient } from "pg";
|
|
3
|
+
import { type SqlDriver } from "./sql-driver.js";
|
|
4
|
+
export type ApprovalStatus = "pending" | "approved" | "denied" | "expired";
|
|
5
|
+
export interface ApprovalRow {
|
|
6
|
+
id: string;
|
|
7
|
+
taskId: string | null;
|
|
8
|
+
sessionId: string | null;
|
|
9
|
+
owner: string | null;
|
|
10
|
+
scope: string | null;
|
|
11
|
+
toolName: string;
|
|
12
|
+
args: unknown;
|
|
13
|
+
status: ApprovalStatus;
|
|
14
|
+
reason: string | null;
|
|
15
|
+
decidedBy: string | null;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
decidedAt: string | null;
|
|
18
|
+
}
|
|
19
|
+
export declare const PG_APPROVAL_SCHEMA: string[];
|
|
20
|
+
export declare function ensureSchema(pool: PgPool | PoolClient): Promise<void>;
|
|
21
|
+
export declare class SqlApprovalStore {
|
|
22
|
+
protected readonly db: SqlDriver;
|
|
23
|
+
constructor(db: SqlDriver);
|
|
24
|
+
private q;
|
|
25
|
+
private json;
|
|
26
|
+
createPending(id: string, scope: string | null, ctx: {
|
|
27
|
+
taskId?: string | null;
|
|
28
|
+
sessionId?: string | null;
|
|
29
|
+
owner?: string | null;
|
|
30
|
+
toolName: string;
|
|
31
|
+
args: unknown;
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
getStatus(id: string, scope: string | null): Promise<{
|
|
34
|
+
status: ApprovalStatus;
|
|
35
|
+
reason: string | null;
|
|
36
|
+
decidedBy: string | null;
|
|
37
|
+
} | undefined>;
|
|
38
|
+
decide(id: string, scope: string | null, decision: "approved" | "denied" | "expired", reason: string | null, decidedBy: string | null): Promise<boolean>;
|
|
39
|
+
listPending(scope: string | null, owner?: string): Promise<ApprovalRow[]>;
|
|
40
|
+
get(id: string, scope: string | null): Promise<ApprovalRow | undefined>;
|
|
41
|
+
getById(id: string): Promise<ApprovalRow | undefined>;
|
|
42
|
+
listPendingAll(): Promise<ApprovalRow[]>;
|
|
43
|
+
expireStale(olderThanMs: number): Promise<number>;
|
|
44
|
+
}
|
|
45
|
+
export declare function mapRow(r: Record<string, unknown>): ApprovalRow;
|
|
46
|
+
export declare class TiDBApprovalStore extends SqlApprovalStore {
|
|
47
|
+
constructor(pool: MySqlPool);
|
|
48
|
+
}
|
|
49
|
+
export declare class PgApprovalStore extends SqlApprovalStore {
|
|
50
|
+
constructor(pool: PgPool);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=approval-store-sql.d.ts.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { parseJsonStrict as parseJson, toIsoOrNull as iso } from "./sql-row-helpers.js";
|
|
2
|
+
import { pgProtocolJsonStringify } from "./pg-safe-json.js";
|
|
3
|
+
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
4
|
+
export const PG_APPROVAL_SCHEMA = [
|
|
5
|
+
`CREATE TABLE IF NOT EXISTS approval (
|
|
6
|
+
id VARCHAR(64) NOT NULL,
|
|
7
|
+
task_id VARCHAR(64),
|
|
8
|
+
session_id VARCHAR(64),
|
|
9
|
+
owner VARCHAR(190),
|
|
10
|
+
-- Single-DB fleet scope guard (tenant isolation). NULLABLE on purpose: a NULL-scope row is an untenanted
|
|
11
|
+
-- run, and every read/decide path matches it with \`scope IS NOT DISTINCT FROM $n\` (the PG twin of TiDB
|
|
12
|
+
-- \`<=>\`) so NULL matches only NULL and one tenant's scope NEVER matches another's.
|
|
13
|
+
scope VARCHAR(190),
|
|
14
|
+
tool_name VARCHAR(190) NOT NULL,
|
|
15
|
+
args JSONB,
|
|
16
|
+
status VARCHAR(16) NOT NULL,
|
|
17
|
+
reason TEXT,
|
|
18
|
+
decided_by VARCHAR(190),
|
|
19
|
+
created_at TIMESTAMPTZ(3) NOT NULL,
|
|
20
|
+
decided_at TIMESTAMPTZ(3),
|
|
21
|
+
PRIMARY KEY (id)
|
|
22
|
+
)`,
|
|
23
|
+
`CREATE INDEX IF NOT EXISTS idx_approval_owner_status ON approval (owner, status)`,
|
|
24
|
+
`CREATE INDEX IF NOT EXISTS idx_approval_status ON approval (status)`,
|
|
25
|
+
`CREATE INDEX IF NOT EXISTS idx_approval_scope_status ON approval (scope, status)`,
|
|
26
|
+
];
|
|
27
|
+
export async function ensureSchema(pool) {
|
|
28
|
+
for (const stmt of PG_APPROVAL_SCHEMA)
|
|
29
|
+
await pool.query(stmt);
|
|
30
|
+
}
|
|
31
|
+
export class SqlApprovalStore {
|
|
32
|
+
db;
|
|
33
|
+
constructor(db) {
|
|
34
|
+
this.db = db;
|
|
35
|
+
}
|
|
36
|
+
q(tidb, pg) {
|
|
37
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
38
|
+
}
|
|
39
|
+
json(value, label) {
|
|
40
|
+
return this.db.dialect === "tidb" ? JSON.stringify(value) : pgProtocolJsonStringify(value, label);
|
|
41
|
+
}
|
|
42
|
+
async createPending(id, scope, ctx) {
|
|
43
|
+
await this.db.query(this.q("INSERT INTO approval (id, task_id, session_id, owner, scope, tool_name, args, status, created_at) " +
|
|
44
|
+
"VALUES (?,?,?,?,?,?,?,'pending',?)", "INSERT INTO approval (id, task_id, session_id, owner, scope, tool_name, args, status, created_at) " +
|
|
45
|
+
"VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,'pending',$8)"), [
|
|
46
|
+
id,
|
|
47
|
+
ctx.taskId ?? null,
|
|
48
|
+
ctx.sessionId ?? null,
|
|
49
|
+
ctx.owner ?? null,
|
|
50
|
+
scope,
|
|
51
|
+
ctx.toolName,
|
|
52
|
+
ctx.args == null ? null : this.json(ctx.args, "approval args"),
|
|
53
|
+
new Date(),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
async getStatus(id, scope) {
|
|
57
|
+
const { rows } = await this.db.query(this.q("SELECT status, reason, decided_by FROM approval WHERE id = ? AND scope <=> ?", "SELECT status, reason, decided_by FROM approval WHERE id = $1 AND scope IS NOT DISTINCT FROM $2"), [id, scope]);
|
|
58
|
+
const r = rows[0];
|
|
59
|
+
if (!r)
|
|
60
|
+
return undefined;
|
|
61
|
+
return { status: r.status, reason: r.reason ?? null, decidedBy: r.decided_by ?? null };
|
|
62
|
+
}
|
|
63
|
+
async decide(id, scope, decision, reason, decidedBy) {
|
|
64
|
+
const res = await this.db.query(this.q("UPDATE approval SET status = ?, reason = ?, decided_by = ?, decided_at = ? WHERE id = ? AND scope <=> ? AND status = 'pending'", "UPDATE approval SET status = $1, reason = $2, decided_by = $3, decided_at = $4 WHERE id = $5 AND scope IS NOT DISTINCT FROM $6 AND status = 'pending'"), [decision, reason, decidedBy, new Date(), id, scope]);
|
|
65
|
+
return res.affected === 1;
|
|
66
|
+
}
|
|
67
|
+
async listPending(scope, owner) {
|
|
68
|
+
const { rows } = owner
|
|
69
|
+
? await this.db.query(this.q("SELECT * FROM approval WHERE status = 'pending' AND scope <=> ? AND owner = ? ORDER BY created_at ASC", "SELECT * FROM approval WHERE status = 'pending' AND scope IS NOT DISTINCT FROM $1 AND owner = $2 ORDER BY created_at ASC"), [scope, owner])
|
|
70
|
+
: await this.db.query(this.q("SELECT * FROM approval WHERE status = 'pending' AND scope <=> ? ORDER BY created_at ASC", "SELECT * FROM approval WHERE status = 'pending' AND scope IS NOT DISTINCT FROM $1 ORDER BY created_at ASC"), [scope]);
|
|
71
|
+
return rows.map(mapRow);
|
|
72
|
+
}
|
|
73
|
+
async get(id, scope) {
|
|
74
|
+
const { rows } = await this.db.query(this.q("SELECT * FROM approval WHERE id = ? AND scope <=> ?", "SELECT * FROM approval WHERE id = $1 AND scope IS NOT DISTINCT FROM $2"), [id, scope]);
|
|
75
|
+
return rows[0] ? mapRow(rows[0]) : undefined;
|
|
76
|
+
}
|
|
77
|
+
async getById(id) {
|
|
78
|
+
const { rows } = await this.db.query(this.q("SELECT * FROM approval WHERE id = ?", "SELECT * FROM approval WHERE id = $1"), [id]);
|
|
79
|
+
return rows[0] ? mapRow(rows[0]) : undefined;
|
|
80
|
+
}
|
|
81
|
+
async listPendingAll() {
|
|
82
|
+
const { rows } = await this.db.query("SELECT * FROM approval WHERE status = 'pending' ORDER BY created_at ASC");
|
|
83
|
+
return rows.map(mapRow);
|
|
84
|
+
}
|
|
85
|
+
async expireStale(olderThanMs) {
|
|
86
|
+
const cutoff = new Date(Date.now() - olderThanMs);
|
|
87
|
+
const res = await this.db.query(this.q("UPDATE approval SET status = 'expired', reason = 'timed out before decision', decided_at = ? " +
|
|
88
|
+
"WHERE status = 'pending' AND created_at < ?", "UPDATE approval SET status = 'expired', reason = 'timed out before decision', decided_at = $1 " +
|
|
89
|
+
"WHERE status = 'pending' AND created_at < $2"), [new Date(), cutoff]);
|
|
90
|
+
return res.affected;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function mapRow(r) {
|
|
94
|
+
return {
|
|
95
|
+
id: String(r.id),
|
|
96
|
+
taskId: r.task_id ?? null,
|
|
97
|
+
sessionId: r.session_id ?? null,
|
|
98
|
+
owner: r.owner ?? null,
|
|
99
|
+
scope: r.scope ?? null,
|
|
100
|
+
toolName: String(r.tool_name),
|
|
101
|
+
args: parseJson(r.args),
|
|
102
|
+
status: r.status,
|
|
103
|
+
reason: r.reason ?? null,
|
|
104
|
+
decidedBy: r.decided_by ?? null,
|
|
105
|
+
createdAt: iso(r.created_at),
|
|
106
|
+
decidedAt: iso(r.decided_at),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export class TiDBApprovalStore extends SqlApprovalStore {
|
|
110
|
+
constructor(pool) {
|
|
111
|
+
super(mysqlDriver(pool));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export class PgApprovalStore extends SqlApprovalStore {
|
|
115
|
+
constructor(pool) {
|
|
116
|
+
super(pgDriver(pool));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=approval-store-sql.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
|
+
import type { Pool as PgPool, PoolClient } from "pg";
|
|
3
|
+
import type { BreakerSnapshot, BreakerState } from "@sema-agent/core";
|
|
4
|
+
import { type SqlDriver } from "./sql-driver.js";
|
|
5
|
+
export declare const PG_BREAKER_STATE_SCHEMA: string[];
|
|
6
|
+
export declare function ensureSchema(pool: PgPool | PoolClient): Promise<void>;
|
|
7
|
+
export declare class SqlBreakerState implements BreakerState {
|
|
8
|
+
protected readonly db: SqlDriver;
|
|
9
|
+
private readonly onWriteFail?;
|
|
10
|
+
private cache;
|
|
11
|
+
private timer?;
|
|
12
|
+
private readonly pending;
|
|
13
|
+
private writeFailStreak;
|
|
14
|
+
constructor(db: SqlDriver, onWriteFail?: ((streak: number) => void) | undefined);
|
|
15
|
+
private q;
|
|
16
|
+
get(key: string): BreakerSnapshot | undefined;
|
|
17
|
+
set(key: string, snap: BreakerSnapshot): void;
|
|
18
|
+
private writeThrough;
|
|
19
|
+
refresh(): Promise<void>;
|
|
20
|
+
startRefresh(intervalMs?: number): this;
|
|
21
|
+
stop(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare class TiDBBreakerState extends SqlBreakerState {
|
|
24
|
+
constructor(pool: MySqlPool, onWriteFail?: (streak: number) => void);
|
|
25
|
+
}
|
|
26
|
+
export declare class PgBreakerState extends SqlBreakerState {
|
|
27
|
+
constructor(pool: PgPool, onWriteFail?: (streak: number) => void);
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=breaker-state-sql.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
2
|
+
export const PG_BREAKER_STATE_SCHEMA = [
|
|
3
|
+
`CREATE TABLE IF NOT EXISTS circuit_breaker (
|
|
4
|
+
breaker_key VARCHAR(190) NOT NULL,
|
|
5
|
+
phase VARCHAR(16) NOT NULL,
|
|
6
|
+
failures INTEGER NOT NULL DEFAULT 0,
|
|
7
|
+
opened_at TIMESTAMPTZ(3),
|
|
8
|
+
updated_at TIMESTAMPTZ(3) NOT NULL,
|
|
9
|
+
PRIMARY KEY (breaker_key)
|
|
10
|
+
)`,
|
|
11
|
+
];
|
|
12
|
+
export async function ensureSchema(pool) {
|
|
13
|
+
for (const stmt of PG_BREAKER_STATE_SCHEMA)
|
|
14
|
+
await pool.query(stmt);
|
|
15
|
+
}
|
|
16
|
+
export class SqlBreakerState {
|
|
17
|
+
db;
|
|
18
|
+
onWriteFail;
|
|
19
|
+
cache = new Map();
|
|
20
|
+
timer;
|
|
21
|
+
pending = new Set();
|
|
22
|
+
writeFailStreak = 0;
|
|
23
|
+
constructor(db, onWriteFail) {
|
|
24
|
+
this.db = db;
|
|
25
|
+
this.onWriteFail = onWriteFail;
|
|
26
|
+
}
|
|
27
|
+
q(tidb, pg) {
|
|
28
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
29
|
+
}
|
|
30
|
+
get(key) {
|
|
31
|
+
return this.cache.get(key);
|
|
32
|
+
}
|
|
33
|
+
set(key, snap) {
|
|
34
|
+
this.cache.set(key, snap);
|
|
35
|
+
this.pending.add(key);
|
|
36
|
+
void this.writeThrough(key, snap);
|
|
37
|
+
}
|
|
38
|
+
async writeThrough(key, snap) {
|
|
39
|
+
try {
|
|
40
|
+
await this.db.query(this.q("INSERT INTO circuit_breaker (breaker_key, phase, failures, opened_at, updated_at) VALUES (?,?,?,?,?) " +
|
|
41
|
+
"ON DUPLICATE KEY UPDATE phase=VALUES(phase), failures=VALUES(failures), opened_at=VALUES(opened_at), updated_at=VALUES(updated_at)", "INSERT INTO circuit_breaker (breaker_key, phase, failures, opened_at, updated_at) VALUES ($1,$2,$3,$4,$5) " +
|
|
42
|
+
"ON CONFLICT (breaker_key) DO UPDATE SET phase=EXCLUDED.phase, failures=EXCLUDED.failures, opened_at=EXCLUDED.opened_at, updated_at=EXCLUDED.updated_at"), [key, snap.phase, snap.failures, snap.openedAt != null ? new Date(snap.openedAt) : null, new Date()]);
|
|
43
|
+
this.pending.delete(key);
|
|
44
|
+
if (this.writeFailStreak > 0) {
|
|
45
|
+
this.writeFailStreak = 0;
|
|
46
|
+
try {
|
|
47
|
+
this.onWriteFail?.(0);
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
try {
|
|
54
|
+
this.onWriteFail?.(++this.writeFailStreak);
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async refresh() {
|
|
60
|
+
try {
|
|
61
|
+
const pendingAtStart = new Set(this.pending);
|
|
62
|
+
const guarded = (key) => this.pending.has(key) || pendingAtStart.has(key);
|
|
63
|
+
const { rows } = await this.db.query("SELECT breaker_key, phase, failures, opened_at FROM circuit_breaker");
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
for (const r of rows) {
|
|
66
|
+
const key = String(r.breaker_key);
|
|
67
|
+
seen.add(key);
|
|
68
|
+
if (guarded(key))
|
|
69
|
+
continue;
|
|
70
|
+
this.cache.set(key, {
|
|
71
|
+
phase: r.phase,
|
|
72
|
+
failures: Number(r.failures),
|
|
73
|
+
openedAt: r.opened_at != null ? new Date(r.opened_at).getTime() : undefined,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
for (const key of this.cache.keys())
|
|
77
|
+
if (!seen.has(key) && !guarded(key))
|
|
78
|
+
this.cache.delete(key);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
startRefresh(intervalMs = 5_000) {
|
|
84
|
+
this.timer ??= setInterval(() => void this.refresh(), intervalMs);
|
|
85
|
+
this.timer.unref?.();
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
stop() {
|
|
89
|
+
if (this.timer)
|
|
90
|
+
clearInterval(this.timer);
|
|
91
|
+
this.timer = undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export class TiDBBreakerState extends SqlBreakerState {
|
|
95
|
+
constructor(pool, onWriteFail) {
|
|
96
|
+
super(mysqlDriver(pool), onWriteFail);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export class PgBreakerState extends SqlBreakerState {
|
|
100
|
+
constructor(pool, onWriteFail) {
|
|
101
|
+
super(pgDriver(pool), onWriteFail);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=breaker-state-sql.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type FileSnapshotBounds, type FileSnapshotStore, type FileSnapshotResult, type ExecutionEnv } from "@sema-agent/core";
|
|
2
|
+
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
3
|
+
import type { Pool as PgPool } from "pg";
|
|
4
|
+
import { type BlobBackend } from "./blob-backend.js";
|
|
5
|
+
import { type SqlDriver } from "./sql-driver.js";
|
|
6
|
+
export declare class SqlFileSnapshotStore implements FileSnapshotStore {
|
|
7
|
+
protected readonly db: SqlDriver;
|
|
8
|
+
protected readonly pool: MySqlPool | PgPool;
|
|
9
|
+
protected readonly bounds: FileSnapshotBounds;
|
|
10
|
+
private readonly inFlight;
|
|
11
|
+
private readonly blobs;
|
|
12
|
+
onSkippedVanished: ((count: number) => void | Promise<void>) | undefined;
|
|
13
|
+
constructor(db: SqlDriver, pool: MySqlPool | PgPool, blobBackend?: BlobBackend, bounds?: FileSnapshotBounds);
|
|
14
|
+
private q;
|
|
15
|
+
private putBlobRow;
|
|
16
|
+
putBlob(hash: string, bytes: Uint8Array): Promise<FileSnapshotResult>;
|
|
17
|
+
snapshot(scope: string, key: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileSnapshotResult>;
|
|
18
|
+
importManifest(scope: string, key: string, manifest: Map<string, string>, srcGetBlob: (hash: string) => Promise<Uint8Array | undefined>): Promise<FileSnapshotResult>;
|
|
19
|
+
restore(scope: string, key: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileSnapshotResult>;
|
|
20
|
+
has(scope: string, key: string): Promise<boolean>;
|
|
21
|
+
listKeys(scope: string): Promise<string[]>;
|
|
22
|
+
exportManifest(scope: string, key: string): Promise<Map<string, string> | null>;
|
|
23
|
+
getBlob(hash: string): Promise<Uint8Array | undefined>;
|
|
24
|
+
hasBlobs(hashes: string[]): Promise<Set<string>>;
|
|
25
|
+
blobSizes(hashes: string[]): Promise<Map<string, number>>;
|
|
26
|
+
reap(scope: string, keepKeys: string[]): Promise<number>;
|
|
27
|
+
deleteBySession(scope: string): Promise<number>;
|
|
28
|
+
private gcOrphanBlobs;
|
|
29
|
+
sweepOrphanBlobs(): Promise<number>;
|
|
30
|
+
private tx;
|
|
31
|
+
}
|
|
32
|
+
export declare class TiDBFileSnapshotStore extends SqlFileSnapshotStore {
|
|
33
|
+
constructor(pool: MySqlPool, blobBackend?: BlobBackend, bounds?: FileSnapshotBounds);
|
|
34
|
+
}
|
|
35
|
+
export declare class PgFileSnapshotStore extends SqlFileSnapshotStore {
|
|
36
|
+
constructor(pool: PgPool, blobBackend?: BlobBackend, bounds?: FileSnapshotBounds);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=file-snapshot-store-sql.d.ts.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { captureManifest, applyManifest, DEFAULT_SNAPSHOT_BOUNDS } from "@sema-agent/core";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { SqlBlobBackend, sqlBlobSizes } from "./blob-backend.js";
|
|
4
|
+
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
5
|
+
export class SqlFileSnapshotStore {
|
|
6
|
+
db;
|
|
7
|
+
pool;
|
|
8
|
+
bounds;
|
|
9
|
+
inFlight = new Set();
|
|
10
|
+
blobs;
|
|
11
|
+
onSkippedVanished = undefined;
|
|
12
|
+
constructor(db, pool, blobBackend, bounds = DEFAULT_SNAPSHOT_BOUNDS) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
this.pool = pool;
|
|
15
|
+
this.bounds = bounds;
|
|
16
|
+
this.blobs = blobBackend ?? new SqlBlobBackend(this.db.dialect, this.pool);
|
|
17
|
+
}
|
|
18
|
+
q(tidb, pg) {
|
|
19
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
20
|
+
}
|
|
21
|
+
async putBlobRow(hash, bytes) {
|
|
22
|
+
await this.blobs.putBlob(hash, bytes);
|
|
23
|
+
}
|
|
24
|
+
async putBlob(hash, bytes) {
|
|
25
|
+
if (createHash("sha256").update(bytes).digest("hex") !== hash) {
|
|
26
|
+
return { ok: false, error: { code: "read_failed", message: `putBlob ${hash} fails content-address integrity — nothing stored` } };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
await this.putBlobRow(hash, bytes);
|
|
30
|
+
return { ok: true };
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
return { ok: false, error: { code: "restore_failed", message: `putBlob ${hash} write/upload failed: ${err.message}` } };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async snapshot(scope, key, env, root, signal) {
|
|
37
|
+
const flightKey = `${scope}\x00${key}`;
|
|
38
|
+
if (this.inFlight.has(flightKey))
|
|
39
|
+
return { ok: true };
|
|
40
|
+
this.inFlight.add(flightKey);
|
|
41
|
+
try {
|
|
42
|
+
if (await this.has(scope, key))
|
|
43
|
+
return { ok: true };
|
|
44
|
+
let cap;
|
|
45
|
+
try {
|
|
46
|
+
cap = await captureManifest(env, root, this.bounds, signal, async (hash, bytes) => {
|
|
47
|
+
await this.putBlobRow(hash, bytes);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return { ok: false, error: { code: "restore_failed", message: `snapshot blob write failed: ${err.message}` } };
|
|
52
|
+
}
|
|
53
|
+
if (!cap.ok)
|
|
54
|
+
return { ok: false, error: cap.error };
|
|
55
|
+
if (cap.skippedVanished > 0 && this.onSkippedVanished) {
|
|
56
|
+
try {
|
|
57
|
+
void Promise.resolve(this.onSkippedVanished(cap.skippedVanished)).catch(() => { });
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
}
|
|
61
|
+
await this.tx(async (conn) => {
|
|
62
|
+
const exists = await conn.query(this.q("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", "SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1"), [scope, key]);
|
|
63
|
+
if (exists.rows[0])
|
|
64
|
+
return;
|
|
65
|
+
for (const [relPath, hash] of cap.value) {
|
|
66
|
+
await conn.query(this.q("INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES (?,?,?,?)", "INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES ($1,$2,$3,$4)"), [scope, key, relPath, hash]);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return { ok: true };
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return { ok: false, error: { code: "restore_failed", message: `snapshot persist failed: ${err.message}` } };
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
this.inFlight.delete(flightKey);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async importManifest(scope, key, manifest, srcGetBlob) {
|
|
79
|
+
const flightKey = `${scope}\x00${key}`;
|
|
80
|
+
if (this.inFlight.has(flightKey))
|
|
81
|
+
return { ok: true };
|
|
82
|
+
this.inFlight.add(flightKey);
|
|
83
|
+
try {
|
|
84
|
+
if (await this.has(scope, key))
|
|
85
|
+
return { ok: true };
|
|
86
|
+
for (const hash of new Set(manifest.values())) {
|
|
87
|
+
let bytes;
|
|
88
|
+
try {
|
|
89
|
+
bytes = await srcGetBlob(hash);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
return { ok: false, error: { code: "read_failed", message: `source blob ${hash} fetch failed: ${err.message}` } };
|
|
93
|
+
}
|
|
94
|
+
if (!bytes)
|
|
95
|
+
return { ok: false, error: { code: "read_failed", message: `missing source blob ${hash}` } };
|
|
96
|
+
if (createHash("sha256").update(bytes).digest("hex") !== hash) {
|
|
97
|
+
return { ok: false, error: { code: "read_failed", message: `source blob ${hash} fails content-address integrity` } };
|
|
98
|
+
}
|
|
99
|
+
await this.putBlobRow(hash, bytes);
|
|
100
|
+
}
|
|
101
|
+
await this.tx(async (conn) => {
|
|
102
|
+
const exists = await conn.query(this.q("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", "SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1"), [scope, key]);
|
|
103
|
+
if (exists.rows[0])
|
|
104
|
+
return;
|
|
105
|
+
for (const [relPath, hash] of manifest) {
|
|
106
|
+
await conn.query(this.q("INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES (?,?,?,?)", "INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES ($1,$2,$3,$4)"), [scope, key, relPath, hash]);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
return { ok: true };
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
return { ok: false, error: { code: "restore_failed", message: `import persist failed: ${err.message}` } };
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
this.inFlight.delete(flightKey);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async restore(scope, key, env, root, signal) {
|
|
119
|
+
try {
|
|
120
|
+
const { rows } = await this.db.query(this.q("SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = ? AND snap_key = ?", "SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2"), [scope, key]);
|
|
121
|
+
if (rows.length === 0)
|
|
122
|
+
return { ok: false, error: { code: "not_found", message: `no file snapshot for ${scope}/${key}` } };
|
|
123
|
+
const manifest = new Map(rows.map((r) => [String(r.rel_path), String(r.blob_hash)]));
|
|
124
|
+
return await applyManifest(env, root, this.bounds, signal, manifest, (hash) => this.blobs.getBlob(hash));
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return { ok: false, error: { code: "restore_failed", message: `restore blob read failed: ${err.message}` } };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async has(scope, key) {
|
|
131
|
+
const { rows } = await this.db.query(this.q("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", "SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1"), [scope, key]);
|
|
132
|
+
return Boolean(rows[0]);
|
|
133
|
+
}
|
|
134
|
+
async listKeys(scope) {
|
|
135
|
+
const { rows } = await this.db.query(this.q("SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = ?", "SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = $1"), [scope]);
|
|
136
|
+
return rows.map((r) => String(r.snap_key));
|
|
137
|
+
}
|
|
138
|
+
async exportManifest(scope, key) {
|
|
139
|
+
const { rows } = await this.db.query(this.q("SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = ? AND snap_key = ?", "SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2"), [scope, key]);
|
|
140
|
+
if (rows.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
return new Map(rows.map((r) => [String(r.rel_path), String(r.blob_hash)]));
|
|
143
|
+
}
|
|
144
|
+
async getBlob(hash) {
|
|
145
|
+
return this.blobs.getBlob(hash);
|
|
146
|
+
}
|
|
147
|
+
async hasBlobs(hashes) {
|
|
148
|
+
return this.blobs.hasBlobs(hashes);
|
|
149
|
+
}
|
|
150
|
+
async blobSizes(hashes) {
|
|
151
|
+
return sqlBlobSizes(this.db.dialect, this.pool, hashes);
|
|
152
|
+
}
|
|
153
|
+
async reap(scope, keepKeys) {
|
|
154
|
+
return this.tx(async (conn) => {
|
|
155
|
+
const { rows: keys } = await conn.query(this.q("SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = ?", "SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = $1"), [scope]);
|
|
156
|
+
const drop = keys.map((k) => String(k.snap_key)).filter((k) => !keepKeys.includes(k));
|
|
157
|
+
for (const k of drop) {
|
|
158
|
+
await conn.query(this.q("DELETE FROM snapshot_manifest WHERE scope = ? AND snap_key = ?", "DELETE FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2"), [scope, k]);
|
|
159
|
+
}
|
|
160
|
+
return drop.length;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async deleteBySession(scope) {
|
|
164
|
+
const { affected } = await this.db.query(this.q("DELETE FROM snapshot_manifest WHERE scope = ?", "DELETE FROM snapshot_manifest WHERE scope = $1"), [scope]);
|
|
165
|
+
return affected;
|
|
166
|
+
}
|
|
167
|
+
async gcOrphanBlobs() {
|
|
168
|
+
const { rows } = await this.db
|
|
169
|
+
.query("SELECT blob_hash FROM snapshot_blob WHERE blob_hash NOT IN (SELECT blob_hash FROM snapshot_manifest)")
|
|
170
|
+
.catch(() => ({ rows: [], affected: 0 }));
|
|
171
|
+
const orphans = rows.map((r) => String(r.blob_hash));
|
|
172
|
+
if (orphans.length === 0)
|
|
173
|
+
return 0;
|
|
174
|
+
return this.blobs.deleteBlobs(orphans);
|
|
175
|
+
}
|
|
176
|
+
async sweepOrphanBlobs() {
|
|
177
|
+
return this.gcOrphanBlobs();
|
|
178
|
+
}
|
|
179
|
+
async tx(fn) {
|
|
180
|
+
const conn = await this.db.connect();
|
|
181
|
+
try {
|
|
182
|
+
await conn.begin();
|
|
183
|
+
const out = await fn(conn);
|
|
184
|
+
await conn.commit();
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
try {
|
|
189
|
+
await conn.rollback();
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
}
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
conn.release();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export class TiDBFileSnapshotStore extends SqlFileSnapshotStore {
|
|
201
|
+
constructor(pool, blobBackend, bounds = DEFAULT_SNAPSHOT_BOUNDS) {
|
|
202
|
+
super(mysqlDriver(pool), pool, blobBackend, bounds);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export class PgFileSnapshotStore extends SqlFileSnapshotStore {
|
|
206
|
+
constructor(pool, blobBackend, bounds = DEFAULT_SNAPSHOT_BOUNDS) {
|
|
207
|
+
super(pgDriver(pool), pool, blobBackend, bounds);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=file-snapshot-store-sql.js.map
|