@dotteamdev/sensdb 0.1.0 → 0.3.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 +42 -3
- package/dist/sens.d.ts +47 -1
- package/dist/sens.js +89 -18
- package/package.json +1 -1
- package/sens.ts +146 -19
package/README.md
CHANGED
|
@@ -9,15 +9,18 @@ Node 18+, Bun, Deno, and any runtime with a global `fetch`.
|
|
|
9
9
|
|
|
10
10
|
## Setup
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
```bash
|
|
13
|
+
npm install @dotteamdev/sensdb
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Start a server:
|
|
14
17
|
|
|
15
18
|
```bash
|
|
16
19
|
sens serve app.db --port 8080 --token "$(openssl rand -hex 16)"
|
|
17
20
|
```
|
|
18
21
|
|
|
19
22
|
```ts
|
|
20
|
-
import { createClient } from "
|
|
23
|
+
import { createClient } from "@dotteamdev/sensdb";
|
|
21
24
|
|
|
22
25
|
const db = createClient(process.env.SENS_URL!, { token: process.env.SENS_TOKEN });
|
|
23
26
|
|
|
@@ -79,11 +82,47 @@ export class SensService {
|
|
|
79
82
|
}
|
|
80
83
|
```
|
|
81
84
|
|
|
85
|
+
## Transactions
|
|
86
|
+
|
|
87
|
+
Group statements into one atomic unit. `transaction()` commits if your callback
|
|
88
|
+
resolves and rolls back if it throws:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
await db.transaction(async (tx) => {
|
|
92
|
+
await tx.query("UPDATE accounts SET cents = cents - ? WHERE id = ?", [100, 1]);
|
|
93
|
+
await tx.query("UPDATE accounts SET cents = cents + ? WHERE id = ?", [100, 2]);
|
|
94
|
+
// resolves → COMMIT; throws → ROLLBACK (both accounts, atomically)
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Manual control, savepoints, and **`PREVIEW`** — a dry-run that returns the exact
|
|
99
|
+
change-set the transaction *would* apply, so an AI agent can inspect before
|
|
100
|
+
committing (no other database offers this as a built-in):
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const tx = await db.begin();
|
|
104
|
+
await tx.query("DELETE FROM users WHERE last_seen < ?", [cutoff]);
|
|
105
|
+
await tx.savepoint("s1");
|
|
106
|
+
await tx.query("UPDATE orders SET status = 'archived' WHERE user_id IN (SELECT id FROM users)");
|
|
107
|
+
|
|
108
|
+
const change = await tx.preview(); // or tx.preview(true) for per-row old→new
|
|
109
|
+
// change.rows = what COMMIT would apply, WITHOUT applying it — inspect it, then:
|
|
110
|
+
if (looksRight(change)) await tx.commit();
|
|
111
|
+
else await tx.rollback();
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The server serializes write transactions (one at a time); a concurrent write
|
|
115
|
+
while a transaction is open throws `SensError` with `status === 409`. Atomicity
|
|
116
|
+
covers relational rows **and** the vector/semantic index together.
|
|
117
|
+
|
|
82
118
|
## API
|
|
83
119
|
|
|
84
120
|
- `createClient(baseUrl, { token?, fetch? })` → `SensClient`
|
|
85
121
|
- `client.query(sql, params?)` → `Promise<{ columns, rows }>` — throws `SensError` on failure
|
|
122
|
+
- `client.begin()` → `Promise<SensTransaction>` — open a transaction (you must `commit()`/`rollback()`)
|
|
123
|
+
- `client.transaction(fn)` → runs `fn(tx)`, commits on resolve, rolls back on throw
|
|
86
124
|
- `client.health()` → `Promise<{ status, version }>`
|
|
125
|
+
- **`SensTransaction`**: `query`, `preview(full?)`, `savepoint(name)`, `rollbackTo(name)`, `release(name)`, `commit()`, `rollback()`
|
|
87
126
|
|
|
88
127
|
Value mapping (JSON ↔ Sens): number ↔ INTEGER/REAL, string ↔ TEXT,
|
|
89
128
|
boolean ↔ BOOLEAN, null ↔ NULL, `number[]` ↔ VECTOR.
|
package/dist/sens.d.ts
CHANGED
|
@@ -17,9 +17,55 @@ export declare class SensError extends Error {
|
|
|
17
17
|
readonly status?: number | undefined;
|
|
18
18
|
constructor(message: string, status?: number | undefined);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* An open multi-statement transaction. Every call is routed to the server's
|
|
22
|
+
* transaction session via the `X-Sens-Txn` token, so the statements form one
|
|
23
|
+
* atomic unit. Finish with `commit()` or `rollback()`. Prefer `transaction()`,
|
|
24
|
+
* which does that for you.
|
|
25
|
+
*
|
|
26
|
+
* The server allows one write transaction at a time; a concurrent write while a
|
|
27
|
+
* transaction is open rejects with `SensError` (HTTP 409). An idle transaction
|
|
28
|
+
* is rolled back by the server after its idle timeout.
|
|
29
|
+
*/
|
|
30
|
+
export interface SensTransaction {
|
|
31
|
+
/** Run a (parameterized) statement inside the transaction. */
|
|
32
|
+
query(sql: string, params?: SensValue[]): Promise<SensResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Dry-run: return the change-set this transaction WOULD apply, without
|
|
35
|
+
* committing. `full` (→ `PREVIEW FULL`) adds per-row old→new detail.
|
|
36
|
+
* Lets an agent inspect the pending changes before deciding to commit.
|
|
37
|
+
*/
|
|
38
|
+
preview(full?: boolean): Promise<SensResult>;
|
|
39
|
+
/** Establish a savepoint. */
|
|
40
|
+
savepoint(name: string): Promise<void>;
|
|
41
|
+
/** Roll back to a savepoint, discarding work after it (the savepoint stays active). */
|
|
42
|
+
rollbackTo(name: string): Promise<void>;
|
|
43
|
+
/** Release a savepoint, merging its work into the parent. */
|
|
44
|
+
release(name: string): Promise<void>;
|
|
45
|
+
/** Commit the whole transaction. Durable on success. */
|
|
46
|
+
commit(): Promise<void>;
|
|
47
|
+
/** Roll back the whole transaction, discarding everything since it began. */
|
|
48
|
+
rollback(): Promise<void>;
|
|
49
|
+
}
|
|
20
50
|
export interface SensClient {
|
|
21
|
-
/** Run a (parameterized) SQL statement. `?` placeholders are filled from `params`. */
|
|
51
|
+
/** Run a (parameterized) SQL statement (autocommit). `?` placeholders are filled from `params`. */
|
|
22
52
|
query(sql: string, params?: SensValue[]): Promise<SensResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Open a multi-statement transaction. Returns a handle whose statements run
|
|
55
|
+
* atomically; you MUST `commit()` or `rollback()` it. Prefer `transaction()`.
|
|
56
|
+
*/
|
|
57
|
+
begin(): Promise<SensTransaction>;
|
|
58
|
+
/**
|
|
59
|
+
* Run `fn` inside a transaction: commits if it resolves, rolls back if it
|
|
60
|
+
* throws (then rethrows). The idiomatic way to use transactions.
|
|
61
|
+
*
|
|
62
|
+
* const inserted = await db.transaction(async (tx) => {
|
|
63
|
+
* await tx.query("INSERT INTO accounts (id, cents) VALUES (?, ?)", [1, 100]);
|
|
64
|
+
* await tx.query("UPDATE ledger SET total = total + ? WHERE id = ?", [100, 1]);
|
|
65
|
+
* return 1;
|
|
66
|
+
* });
|
|
67
|
+
*/
|
|
68
|
+
transaction<T>(fn: (tx: SensTransaction) => Promise<T>): Promise<T>;
|
|
23
69
|
/** Server health + version. */
|
|
24
70
|
health(): Promise<{
|
|
25
71
|
status: string;
|
package/dist/sens.js
CHANGED
|
@@ -13,6 +13,12 @@ export class SensError extends Error {
|
|
|
13
13
|
this.name = "SensError";
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
/** Savepoint names are SQL identifiers and can't be parameterized, so validate them. */
|
|
17
|
+
function assertIdentifier(name) {
|
|
18
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
19
|
+
throw new SensError(`invalid savepoint name: ${JSON.stringify(name)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
16
22
|
/**
|
|
17
23
|
* Create a Sens client bound to a `sens serve` base URL, e.g.
|
|
18
24
|
* const db = createClient("http://127.0.0.1:8080", { token: process.env.SENS_TOKEN });
|
|
@@ -20,30 +26,94 @@ export class SensError extends Error {
|
|
|
20
26
|
export function createClient(baseUrl, opts = {}) {
|
|
21
27
|
const url = baseUrl.replace(/\/+$/, "");
|
|
22
28
|
const doFetch = opts.fetch ?? fetch;
|
|
23
|
-
const
|
|
29
|
+
const baseHeaders = { "content-type": "application/json" };
|
|
24
30
|
if (opts.token) {
|
|
25
|
-
|
|
31
|
+
baseHeaders["authorization"] = `Bearer ${opts.token}`;
|
|
32
|
+
}
|
|
33
|
+
/** POST /query, optionally within a transaction session (txnToken). Returns the parsed body. */
|
|
34
|
+
async function request(sql, params, txnToken) {
|
|
35
|
+
const headers = { ...baseHeaders };
|
|
36
|
+
if (txnToken) {
|
|
37
|
+
headers["x-sens-txn"] = txnToken;
|
|
38
|
+
}
|
|
39
|
+
const res = await doFetch(`${url}/query`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers,
|
|
42
|
+
body: JSON.stringify({ sql, params }),
|
|
43
|
+
});
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
let data;
|
|
46
|
+
try {
|
|
47
|
+
data = JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new SensError(`invalid response from server: ${text.slice(0, 200)}`, res.status);
|
|
51
|
+
}
|
|
52
|
+
const obj = data;
|
|
53
|
+
if (!res.ok || obj.error) {
|
|
54
|
+
throw new SensError(obj.error ?? `HTTP ${res.status}`, res.status);
|
|
55
|
+
}
|
|
56
|
+
return obj;
|
|
57
|
+
}
|
|
58
|
+
function toResult(obj) {
|
|
59
|
+
return { columns: obj.columns ?? [], rows: obj.rows ?? [] };
|
|
60
|
+
}
|
|
61
|
+
function makeTransaction(token) {
|
|
62
|
+
return {
|
|
63
|
+
async query(sql, params = []) {
|
|
64
|
+
return toResult(await request(sql, params, token));
|
|
65
|
+
},
|
|
66
|
+
async preview(full = false) {
|
|
67
|
+
return toResult(await request(full ? "PREVIEW FULL" : "PREVIEW", [], token));
|
|
68
|
+
},
|
|
69
|
+
async savepoint(name) {
|
|
70
|
+
assertIdentifier(name);
|
|
71
|
+
await request(`SAVEPOINT ${name}`, [], token);
|
|
72
|
+
},
|
|
73
|
+
async rollbackTo(name) {
|
|
74
|
+
assertIdentifier(name);
|
|
75
|
+
await request(`ROLLBACK TO ${name}`, [], token);
|
|
76
|
+
},
|
|
77
|
+
async release(name) {
|
|
78
|
+
assertIdentifier(name);
|
|
79
|
+
await request(`RELEASE ${name}`, [], token);
|
|
80
|
+
},
|
|
81
|
+
async commit() {
|
|
82
|
+
await request("COMMIT", [], token);
|
|
83
|
+
},
|
|
84
|
+
async rollback() {
|
|
85
|
+
await request("ROLLBACK", [], token);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
26
88
|
}
|
|
27
|
-
|
|
89
|
+
const client = {
|
|
28
90
|
async query(sql, params = []) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let data;
|
|
36
|
-
try {
|
|
37
|
-
data = JSON.parse(text);
|
|
91
|
+
return toResult(await request(sql, params));
|
|
92
|
+
},
|
|
93
|
+
async begin() {
|
|
94
|
+
const obj = await request("BEGIN", []);
|
|
95
|
+
if (!obj.txn) {
|
|
96
|
+
throw new SensError("server did not return a transaction token for BEGIN");
|
|
38
97
|
}
|
|
39
|
-
|
|
40
|
-
|
|
98
|
+
return makeTransaction(obj.txn);
|
|
99
|
+
},
|
|
100
|
+
async transaction(fn) {
|
|
101
|
+
const tx = await client.begin();
|
|
102
|
+
let result;
|
|
103
|
+
try {
|
|
104
|
+
result = await fn(tx);
|
|
41
105
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
106
|
+
catch (err) {
|
|
107
|
+
try {
|
|
108
|
+
await tx.rollback();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* keep the original error; the server also idle-reaps an abandoned txn */
|
|
112
|
+
}
|
|
113
|
+
throw err;
|
|
45
114
|
}
|
|
46
|
-
|
|
115
|
+
await tx.commit();
|
|
116
|
+
return result;
|
|
47
117
|
},
|
|
48
118
|
async health() {
|
|
49
119
|
const res = await doFetch(`${url}/health`);
|
|
@@ -53,4 +123,5 @@ export function createClient(baseUrl, opts = {}) {
|
|
|
53
123
|
return (await res.json());
|
|
54
124
|
},
|
|
55
125
|
};
|
|
126
|
+
return client;
|
|
56
127
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotteamdev/sensdb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Zero-dependency TypeScript client for a Sens (sens serve) HTTP/JSON endpoint — relational + AI-native (vector/semantic search) database.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/sens.js",
|
package/sens.ts
CHANGED
|
@@ -23,9 +23,56 @@ export class SensError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* An open multi-statement transaction. Every call is routed to the server's
|
|
28
|
+
* transaction session via the `X-Sens-Txn` token, so the statements form one
|
|
29
|
+
* atomic unit. Finish with `commit()` or `rollback()`. Prefer `transaction()`,
|
|
30
|
+
* which does that for you.
|
|
31
|
+
*
|
|
32
|
+
* The server allows one write transaction at a time; a concurrent write while a
|
|
33
|
+
* transaction is open rejects with `SensError` (HTTP 409). An idle transaction
|
|
34
|
+
* is rolled back by the server after its idle timeout.
|
|
35
|
+
*/
|
|
36
|
+
export interface SensTransaction {
|
|
37
|
+
/** Run a (parameterized) statement inside the transaction. */
|
|
38
|
+
query(sql: string, params?: SensValue[]): Promise<SensResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Dry-run: return the change-set this transaction WOULD apply, without
|
|
41
|
+
* committing. `full` (→ `PREVIEW FULL`) adds per-row old→new detail.
|
|
42
|
+
* Lets an agent inspect the pending changes before deciding to commit.
|
|
43
|
+
*/
|
|
44
|
+
preview(full?: boolean): Promise<SensResult>;
|
|
45
|
+
/** Establish a savepoint. */
|
|
46
|
+
savepoint(name: string): Promise<void>;
|
|
47
|
+
/** Roll back to a savepoint, discarding work after it (the savepoint stays active). */
|
|
48
|
+
rollbackTo(name: string): Promise<void>;
|
|
49
|
+
/** Release a savepoint, merging its work into the parent. */
|
|
50
|
+
release(name: string): Promise<void>;
|
|
51
|
+
/** Commit the whole transaction. Durable on success. */
|
|
52
|
+
commit(): Promise<void>;
|
|
53
|
+
/** Roll back the whole transaction, discarding everything since it began. */
|
|
54
|
+
rollback(): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
26
57
|
export interface SensClient {
|
|
27
|
-
/** Run a (parameterized) SQL statement. `?` placeholders are filled from `params`. */
|
|
58
|
+
/** Run a (parameterized) SQL statement (autocommit). `?` placeholders are filled from `params`. */
|
|
28
59
|
query(sql: string, params?: SensValue[]): Promise<SensResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Open a multi-statement transaction. Returns a handle whose statements run
|
|
62
|
+
* atomically; you MUST `commit()` or `rollback()` it. Prefer `transaction()`.
|
|
63
|
+
*/
|
|
64
|
+
begin(): Promise<SensTransaction>;
|
|
65
|
+
/**
|
|
66
|
+
* Run `fn` inside a transaction: commits if it resolves, rolls back if it
|
|
67
|
+
* throws (then rethrows). The idiomatic way to use transactions.
|
|
68
|
+
*
|
|
69
|
+
* const inserted = await db.transaction(async (tx) => {
|
|
70
|
+
* await tx.query("INSERT INTO accounts (id, cents) VALUES (?, ?)", [1, 100]);
|
|
71
|
+
* await tx.query("UPDATE ledger SET total = total + ? WHERE id = ?", [100, 1]);
|
|
72
|
+
* return 1;
|
|
73
|
+
* });
|
|
74
|
+
*/
|
|
75
|
+
transaction<T>(fn: (tx: SensTransaction) => Promise<T>): Promise<T>;
|
|
29
76
|
/** Server health + version. */
|
|
30
77
|
health(): Promise<{ status: string; version: string }>;
|
|
31
78
|
}
|
|
@@ -37,6 +84,13 @@ export interface SensClientOptions {
|
|
|
37
84
|
fetch?: typeof fetch;
|
|
38
85
|
}
|
|
39
86
|
|
|
87
|
+
/** Savepoint names are SQL identifiers and can't be parameterized, so validate them. */
|
|
88
|
+
function assertIdentifier(name: string): void {
|
|
89
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
90
|
+
throw new SensError(`invalid savepoint name: ${JSON.stringify(name)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
40
94
|
/**
|
|
41
95
|
* Create a Sens client bound to a `sens serve` base URL, e.g.
|
|
42
96
|
* const db = createClient("http://127.0.0.1:8080", { token: process.env.SENS_TOKEN });
|
|
@@ -44,30 +98,101 @@ export interface SensClientOptions {
|
|
|
44
98
|
export function createClient(baseUrl: string, opts: SensClientOptions = {}): SensClient {
|
|
45
99
|
const url = baseUrl.replace(/\/+$/, "");
|
|
46
100
|
const doFetch = opts.fetch ?? fetch;
|
|
47
|
-
const
|
|
101
|
+
const baseHeaders: Record<string, string> = { "content-type": "application/json" };
|
|
48
102
|
if (opts.token) {
|
|
49
|
-
|
|
103
|
+
baseHeaders["authorization"] = `Bearer ${opts.token}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** POST /query, optionally within a transaction session (txnToken). Returns the parsed body. */
|
|
107
|
+
async function request(
|
|
108
|
+
sql: string,
|
|
109
|
+
params: SensValue[],
|
|
110
|
+
txnToken?: string,
|
|
111
|
+
): Promise<{ error?: string; columns?: string[]; rows?: SensValue[][]; txn?: string }> {
|
|
112
|
+
const headers: Record<string, string> = { ...baseHeaders };
|
|
113
|
+
if (txnToken) {
|
|
114
|
+
headers["x-sens-txn"] = txnToken;
|
|
115
|
+
}
|
|
116
|
+
const res = await doFetch(`${url}/query`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers,
|
|
119
|
+
body: JSON.stringify({ sql, params }),
|
|
120
|
+
});
|
|
121
|
+
const text = await res.text();
|
|
122
|
+
let data: unknown;
|
|
123
|
+
try {
|
|
124
|
+
data = JSON.parse(text);
|
|
125
|
+
} catch {
|
|
126
|
+
throw new SensError(`invalid response from server: ${text.slice(0, 200)}`, res.status);
|
|
127
|
+
}
|
|
128
|
+
const obj = data as { error?: string; columns?: string[]; rows?: SensValue[][]; txn?: string };
|
|
129
|
+
if (!res.ok || obj.error) {
|
|
130
|
+
throw new SensError(obj.error ?? `HTTP ${res.status}`, res.status);
|
|
131
|
+
}
|
|
132
|
+
return obj;
|
|
50
133
|
}
|
|
51
134
|
|
|
52
|
-
|
|
135
|
+
function toResult(obj: { columns?: string[]; rows?: SensValue[][] }): SensResult {
|
|
136
|
+
return { columns: obj.columns ?? [], rows: obj.rows ?? [] };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function makeTransaction(token: string): SensTransaction {
|
|
140
|
+
return {
|
|
141
|
+
async query(sql: string, params: SensValue[] = []): Promise<SensResult> {
|
|
142
|
+
return toResult(await request(sql, params, token));
|
|
143
|
+
},
|
|
144
|
+
async preview(full = false): Promise<SensResult> {
|
|
145
|
+
return toResult(await request(full ? "PREVIEW FULL" : "PREVIEW", [], token));
|
|
146
|
+
},
|
|
147
|
+
async savepoint(name: string): Promise<void> {
|
|
148
|
+
assertIdentifier(name);
|
|
149
|
+
await request(`SAVEPOINT ${name}`, [], token);
|
|
150
|
+
},
|
|
151
|
+
async rollbackTo(name: string): Promise<void> {
|
|
152
|
+
assertIdentifier(name);
|
|
153
|
+
await request(`ROLLBACK TO ${name}`, [], token);
|
|
154
|
+
},
|
|
155
|
+
async release(name: string): Promise<void> {
|
|
156
|
+
assertIdentifier(name);
|
|
157
|
+
await request(`RELEASE ${name}`, [], token);
|
|
158
|
+
},
|
|
159
|
+
async commit(): Promise<void> {
|
|
160
|
+
await request("COMMIT", [], token);
|
|
161
|
+
},
|
|
162
|
+
async rollback(): Promise<void> {
|
|
163
|
+
await request("ROLLBACK", [], token);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const client: SensClient = {
|
|
53
169
|
async query(sql: string, params: SensValue[] = []): Promise<SensResult> {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
data = JSON.parse(text);
|
|
63
|
-
} catch {
|
|
64
|
-
throw new SensError(`invalid response from server: ${text.slice(0, 200)}`, res.status);
|
|
170
|
+
return toResult(await request(sql, params));
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async begin(): Promise<SensTransaction> {
|
|
174
|
+
const obj = await request("BEGIN", []);
|
|
175
|
+
if (!obj.txn) {
|
|
176
|
+
throw new SensError("server did not return a transaction token for BEGIN");
|
|
65
177
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
178
|
+
return makeTransaction(obj.txn);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async transaction<T>(fn: (tx: SensTransaction) => Promise<T>): Promise<T> {
|
|
182
|
+
const tx = await client.begin();
|
|
183
|
+
let result: T;
|
|
184
|
+
try {
|
|
185
|
+
result = await fn(tx);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
try {
|
|
188
|
+
await tx.rollback();
|
|
189
|
+
} catch {
|
|
190
|
+
/* keep the original error; the server also idle-reaps an abandoned txn */
|
|
191
|
+
}
|
|
192
|
+
throw err;
|
|
69
193
|
}
|
|
70
|
-
|
|
194
|
+
await tx.commit();
|
|
195
|
+
return result;
|
|
71
196
|
},
|
|
72
197
|
|
|
73
198
|
async health(): Promise<{ status: string; version: string }> {
|
|
@@ -78,4 +203,6 @@ export function createClient(baseUrl: string, opts: SensClientOptions = {}): Sen
|
|
|
78
203
|
return (await res.json()) as { status: string; version: string };
|
|
79
204
|
},
|
|
80
205
|
};
|
|
206
|
+
|
|
207
|
+
return client;
|
|
81
208
|
}
|