letopis 1.4.1 → 2.0.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/AGENT-CHEATSHEET.md +329 -278
- package/CHANGELOG.md +558 -1
- package/README.md +606 -1037
- package/dist/acl.d.ts +78 -1
- package/dist/acl.js +264 -13
- package/dist/admin.d.ts +10 -141
- package/dist/admin.js +24 -175
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +5 -15
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +33 -10
- package/dist/chain.d.ts +19 -41
- package/dist/chain.js +169 -201
- package/dist/cli.js +51 -88
- package/dist/demo/booking.d.ts +352 -36
- package/dist/describe.d.ts +47 -0
- package/dist/describe.js +195 -0
- package/dist/errors.d.ts +59 -13
- package/dist/errors.js +190 -29
- package/dist/index.d.ts +52 -71
- package/dist/index.js +245 -130
- package/dist/internal.d.ts +12 -0
- package/dist/internal.js +5 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +20 -12
- package/dist/load.js +225 -186
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +171 -215
- package/dist/messages/access.d.ts +90 -0
- package/dist/messages/access.js +113 -0
- package/dist/messages/api.d.ts +171 -0
- package/dist/messages/api.js +215 -0
- package/dist/messages/boot.d.ts +201 -0
- package/dist/messages/boot.js +256 -0
- package/dist/messages/codes.d.ts +172 -0
- package/dist/messages/codes.js +173 -0
- package/dist/messages/index.d.ts +95 -0
- package/dist/messages/index.js +118 -0
- package/dist/messages/mcp.d.ts +306 -0
- package/dist/messages/mcp.js +387 -0
- package/dist/messages/model.d.ts +233 -0
- package/dist/messages/model.js +283 -0
- package/dist/messages/query.d.ts +131 -0
- package/dist/messages/query.js +158 -0
- package/dist/messages/sql-access.d.ts +70 -0
- package/dist/messages/sql-access.js +87 -0
- package/dist/messages/sql-core.d.ts +150 -0
- package/dist/messages/sql-core.js +181 -0
- package/dist/messages/sql-time.d.ts +71 -0
- package/dist/messages/sql-time.js +88 -0
- package/dist/messages/sql-write.d.ts +246 -0
- package/dist/messages/sql-write.js +270 -0
- package/dist/messages/time.d.ts +23 -0
- package/dist/messages/time.js +29 -0
- package/dist/messages/types.d.ts +27 -0
- package/dist/messages/types.js +5 -0
- package/dist/messages/write.d.ts +405 -0
- package/dist/messages/write.js +509 -0
- package/dist/model.d.ts +73 -60
- package/dist/model.js +26 -29
- package/dist/ops.d.ts +35 -0
- package/dist/ops.js +101 -0
- package/dist/pglite.d.ts +0 -9
- package/dist/pglite.js +3 -1
- package/dist/registry.d.ts +3 -19
- package/dist/registry.js +3 -16
- package/dist/signals.d.ts +5 -9
- package/dist/signals.js +5 -9
- package/dist/sql.d.ts +13 -6
- package/dist/sql.js +176 -83
- package/dist/sync.d.ts +9 -3
- package/dist/sync.js +51 -21
- package/dist/tx.d.ts +26 -1
- package/dist/tx.js +44 -14
- package/dist/typed.d.ts +240 -45
- package/dist/types.d.ts +23 -91
- package/dist/types.js +12 -14
- package/dist/up.d.ts +53 -32
- package/dist/up.js +184 -73
- package/dist/uuid.js +5 -5
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +61 -29
- package/dist/watch.d.ts +11 -10
- package/dist/watch.js +33 -70
- package/dist/write.d.ts +9 -16
- package/dist/write.js +118 -187
- package/llms.txt +22 -25
- package/package.json +8 -11
- package/sql/10-core.sql +7 -50
- package/sql/15-errors.sql +64 -10
- package/sql/20-context.sql +7 -6
- package/sql/30-validate.sql +35 -27
- package/sql/40-class.sql +20 -114
- package/sql/50-acl.sql +8 -13
- package/sql/60-write.sql +220 -667
- package/sql/70-read.sql +20 -16
- package/sql/80-auth.sql +24 -27
- package/sql/90-time.sql +70 -859
- package/sql/95-seed.system.sql +7 -14
- package/sql/99-revision.sql +2 -1
- package/sql/README.md +32 -24
- package/sql/migrate/README.md +14 -0
- package/AGENT-CHEATSHEET.en.md +0 -485
- package/MIGRATION.md +0 -190
- package/README.en.md +0 -2859
- package/dist/import.d.ts +0 -185
- package/dist/import.js +0 -820
- package/dist/project.d.ts +0 -350
- package/dist/project.js +0 -1224
- package/sql/75-history.sql +0 -358
- package/sql/88-derived.sql +0 -1235
- package/sql/migrate/0006-changed-paths.sql +0 -3
- package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/dist/sync.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lerr, lmsg } from './errors.js';
|
|
2
|
+
import { internalsOf } from './internal.js';
|
|
3
|
+
import { RESERVED_CLASS_NAMES } from './types.js';
|
|
2
4
|
const ROLLBACK = Symbol('sync-dry-run');
|
|
3
5
|
/** Канонический JSON: ключи по алфавиту — сравнение описаний без учёта порядка ключей. */
|
|
4
6
|
function canon(v) {
|
|
@@ -9,11 +11,17 @@ function canon(v) {
|
|
|
9
11
|
return JSON.stringify(v);
|
|
10
12
|
}
|
|
11
13
|
export async function syncModels(db, models, opts = {}) {
|
|
14
|
+
// хаб с именем метода цепочки или корня db (модель собрана мимо hub()) — отказ до записи (план 2.0, этап 4, п. 2;
|
|
15
|
+
// 7.10 №3); связь с таким именем отклоняет база
|
|
16
|
+
for (const m of models) {
|
|
17
|
+
if (m.kind === 'hub' && RESERVED_CLASS_NAMES.includes(m.name))
|
|
18
|
+
throw lerr('model.sync_hub_name_reserved', { name: m.name });
|
|
19
|
+
}
|
|
12
20
|
const res = { applied: Boolean(opts.apply), created: [], updated: [], unchanged: [], skipped: [], lost: models.flatMap((m) => m.lostRules()) };
|
|
13
21
|
const S = `"${db.schema.replace(/"/g, '""')}"`;
|
|
14
|
-
// предок раньше
|
|
15
|
-
//
|
|
16
|
-
//
|
|
22
|
+
// предок раньше потомка: потомка класса, которого ещё нет, база отклоняет. Круг родителей (модель, собранная мимо
|
|
23
|
+
// hub()) так не упорядочить — отказ до записи с самим кругом (хвост ревью доработок после 1.2.0: раньше обход молча
|
|
24
|
+
// ставил потомка раньше предка)
|
|
17
25
|
const ordered = [];
|
|
18
26
|
const path = [];
|
|
19
27
|
const visit = (m) => {
|
|
@@ -21,23 +29,21 @@ export async function syncModels(db, models, opts = {}) {
|
|
|
21
29
|
return;
|
|
22
30
|
const at = path.indexOf(m);
|
|
23
31
|
if (at >= 0) {
|
|
24
|
-
throw
|
|
25
|
-
+ 'родителя и источник агрегата база ждёт раньше класса');
|
|
32
|
+
throw lerr('model.models_cycle', { cycle: [...path.slice(at), m].map((x) => x.name).join(' → ') });
|
|
26
33
|
}
|
|
27
34
|
path.push(m);
|
|
28
35
|
if (m.parent && models.includes(m.parent))
|
|
29
36
|
visit(m.parent);
|
|
30
|
-
for (const a of Object.values(m.describe().aggregate ?? {})) {
|
|
31
|
-
const src = models.find((x) => x.name === a.of);
|
|
32
|
-
if (src)
|
|
33
|
-
visit(src);
|
|
34
|
-
}
|
|
35
37
|
path.pop();
|
|
36
38
|
ordered.push(m);
|
|
37
39
|
};
|
|
38
40
|
models.forEach(visit);
|
|
41
|
+
// у корня connect({ admin }) классы пишет администраторский пул с той же сессией (план 2.0, 7.9 №3): правка класса
|
|
42
|
+
// со строками требует прав, которых у роли приложения может не быть
|
|
43
|
+
const adminTx = internalsOf(db)?.adminTx;
|
|
44
|
+
const run = (fn) => (adminTx ? adminTx(fn) : db.transaction(fn));
|
|
39
45
|
try {
|
|
40
|
-
await
|
|
46
|
+
await run(async (tx) => {
|
|
41
47
|
for (const m of ordered) {
|
|
42
48
|
const desc = m.describe();
|
|
43
49
|
const [cur] = await tx.unsafe(`select tenant = ${S}.ctx_tenant() as own, data - 'resolved' as data from ${S}.entity
|
|
@@ -77,8 +83,12 @@ export function schemaToTs(s, indent = '') {
|
|
|
77
83
|
return JSON.stringify(s.const);
|
|
78
84
|
if (Array.isArray(s.enum))
|
|
79
85
|
return s.enum.map((v) => JSON.stringify(v)).join(' | ');
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
const branches = (s.anyOf ?? s.oneOf);
|
|
87
|
+
// ветки из одних required у объекта с полями — условие, а не форма: тип — сам объект (план 2.0, 7.5 №1)
|
|
88
|
+
const onlyRequired = Array.isArray(branches) && s.properties !== undefined
|
|
89
|
+
&& branches.every((b) => Object.keys(b).every((k) => k === 'required'));
|
|
90
|
+
if (Array.isArray(branches) && !onlyRequired)
|
|
91
|
+
return branches.map((b) => schemaToTs(b, indent)).join(' | ');
|
|
82
92
|
const types = Array.isArray(s.type) ? s.type : [s.type];
|
|
83
93
|
return types.map((t) => {
|
|
84
94
|
switch (t) {
|
|
@@ -104,20 +114,40 @@ export function schemaToTs(s, indent = '') {
|
|
|
104
114
|
}
|
|
105
115
|
}).join(' | ');
|
|
106
116
|
}
|
|
107
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Объявления `<Класс>Data` и `<Класс>Links` для классов реестра и `<Класс>Model` — имя, алиас, концы с классами целей
|
|
119
|
+
* и предки (ModelLike, typed.ts), — а в конце список `Models`: строгие типы цепочек без моделей Zod, `connect<Models>()`
|
|
120
|
+
* (план 2.0, этап 4, п. 1). Классы арендатора System (системные и классы сидов пакета) — только с includeSystem
|
|
121
|
+
* (`letopis types --system`); без него системные классы даёт встроенный набор SystemModels. Пометка в первой строке —
|
|
122
|
+
* запись каталога model.types_header.
|
|
123
|
+
*/
|
|
108
124
|
export function generateTypes(registry, opts = {}) {
|
|
109
|
-
const out = [
|
|
125
|
+
const out = [`// ${lmsg('model.types_header')}`, ''];
|
|
126
|
+
const models = [];
|
|
127
|
+
const lit = (v) => JSON.stringify(v);
|
|
110
128
|
for (const c of registry.all().sort((a, b) => a.name.localeCompare(b.name))) {
|
|
111
129
|
if (!opts.includeSystem && c.tenant === '2eba6d0a-1edd-4bb6-a85a-a703dab49035')
|
|
112
130
|
continue;
|
|
113
|
-
|
|
114
|
-
|
|
131
|
+
// идентификатор TypeScript — буквы Unicode, цифры, _ и $: кириллические имена классов допустимы (план 2.0, 7.5 №1)
|
|
132
|
+
if (!/^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(c.name))
|
|
133
|
+
throw lerr('model.types_class_name', { name: c.name });
|
|
115
134
|
// additionalProperties: false верхнего уровня в тип не переносится — лишние ключи и так ошибка типов
|
|
116
135
|
const { additionalProperties: _closed, ...open } = c.schema;
|
|
117
136
|
void _closed;
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
out.push(`export interface ${c.name}
|
|
137
|
+
// объектный литерал — интерфейс; класс без полей, union и открытая схема — псевдоним типа (иначе неверный TypeScript)
|
|
138
|
+
const data = schemaToTs(open);
|
|
139
|
+
out.push(/^\{[\s\S]*\}$/.test(data) && !/\}\s*&/.test(data) ? `export interface ${c.name}Data ${data}` : `export type ${c.name}Data = ${data};`);
|
|
140
|
+
const required = (e) => e.required === true || (e.min ?? 0) > 0;
|
|
141
|
+
const ends = Object.entries(c.ends).map(([role, e]) => ` ${lit(role)}${required(e) ? '' : '?'}: ${e.many ? 'string[]' : 'string'};`);
|
|
142
|
+
out.push(`export interface ${c.name}Links {${ends.length ? `\n${ends.join('\n')}\n` : ''}}`);
|
|
143
|
+
// концы с классами целей и предки — переходы, слоты и семейства строгих типов (typed.ts: ModelLike)
|
|
144
|
+
const specs = Object.entries(c.ends).map(([role, e]) => {
|
|
145
|
+
const targets = (Array.isArray(e.class) ? e.class : [e.class]).map(lit).join(' | ');
|
|
146
|
+
return ` ${lit(role)}: { targets: ${targets}; many: ${e.many === true}; required: ${required(e)} };`;
|
|
147
|
+
});
|
|
148
|
+
out.push(`export interface ${c.name}Model {`, ` readonly name: ${lit(c.name)};`, ...(c.alias ? [` readonly alias: ${lit(c.alias)};`] : []), ' readonly __types?: {', ` data: ${c.name}Data;`, ` links: ${c.name}Links;`, ` ends: {${specs.length ? `\n${specs.join('\n')}\n ` : ''}};`, ` ancestors: ${(c.ancestors.length ? c.ancestors : [c.name]).map(lit).join(' | ')};`, ' };', '}', '');
|
|
149
|
+
models.push(`${c.name}Model`);
|
|
121
150
|
}
|
|
151
|
+
out.push(`export type Models = readonly [${models.join(', ')}];`, '');
|
|
122
152
|
return out.join('\n');
|
|
123
153
|
}
|
package/dist/tx.d.ts
CHANGED
|
@@ -31,6 +31,21 @@ export declare function isTransient(e: unknown): boolean;
|
|
|
31
31
|
export declare function isLogConflict(e: unknown): boolean;
|
|
32
32
|
/** Обрыв связи: сокет закрыт или серверный процесс завершён (FATAL) — исход отправленного неизвестен. */
|
|
33
33
|
export declare function isConnectionError(e: unknown): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Откатить транзакцию на зарезервированном соединении после ошибки: после обрыва в него не пишут (postgres.js 3.4
|
|
36
|
+
* уронил бы процесс необработанным TypeError), иначе — rollback, ошибка которого не важна (план 2.0, 7.4 №7).
|
|
37
|
+
*/
|
|
38
|
+
export declare function rollbackAfter(conn: {
|
|
39
|
+
unsafe(text: string): Promise<unknown>;
|
|
40
|
+
}, err: unknown): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Вернуть зарезервированное соединение в пул. После обрыва сокета пул postgres.js уже перевёл
|
|
43
|
+
* соединение в закрытые (onclose); release() вернул бы его в открытые, и следующая операция
|
|
44
|
+
* писала бы в сокет null — необработанный TypeError роняет процесс.
|
|
45
|
+
*/
|
|
46
|
+
export declare function release(conn: {
|
|
47
|
+
release(): void;
|
|
48
|
+
}, err: unknown): void;
|
|
34
49
|
export declare const RETRIES = 3;
|
|
35
50
|
export declare const retryDelay: (attempt: number) => Promise<void>;
|
|
36
51
|
/** Нотификатор процесса: копит нагрузки сигналов и отправляет их пачкой раз в интервал. */
|
|
@@ -78,6 +93,16 @@ export interface TxContext {
|
|
|
78
93
|
passes?: AclPasses;
|
|
79
94
|
/** После успешной операции (продление сессии раз в минуту). */
|
|
80
95
|
after?: () => void;
|
|
96
|
+
/**
|
|
97
|
+
* Сессия кончилась (no_session): выдать новую и вписать её токен в failed — у подключения по ключу APIKEY ключ
|
|
98
|
+
* обменивается заново. true — операцию можно повторить (один раз). Нет — no_session как есть.
|
|
99
|
+
*/
|
|
100
|
+
renew?: (failed: TxContext) => Promise<boolean>;
|
|
101
|
+
/**
|
|
102
|
+
* Перед операцией (runTx, ExplicitTx.open): после простоя подключение проверяет, жива ли сессия, — иначе чтение под
|
|
103
|
+
* истёкшей сессией шло бы как анонимное и молча отдавало пустой ответ. Обновляет токен ctx, если сессию выдали заново.
|
|
104
|
+
*/
|
|
105
|
+
before?: (ctx: TxContext) => Promise<void>;
|
|
81
106
|
/**
|
|
82
107
|
* После фиксации записи: нагрузки из letopis.changed (сигналы записи, session:<тег>) — кэш
|
|
83
108
|
* результатов сбрасывает свои ответы сразу (§2.12); raw — запись через db.sql.
|
|
@@ -121,7 +146,7 @@ export declare class ExplicitTx implements Executor {
|
|
|
121
146
|
/** В транзакции был сырой SQL (db.sql): после фиксации — весь кэш. */
|
|
122
147
|
private raw;
|
|
123
148
|
private constructor();
|
|
124
|
-
static open(ctx: TxContext): Promise<ExplicitTx>;
|
|
149
|
+
static open(ctx: TxContext, renewed?: boolean): Promise<ExplicitTx>;
|
|
125
150
|
/** Транзакция ещё открыта (не зафиксирована и не откачена). */
|
|
126
151
|
get active(): boolean;
|
|
127
152
|
run<T>(fn: (tx: Tx) => Promise<T>, opts?: {
|
package/dist/tx.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LetopisError, fromDbError } from './errors.js';
|
|
1
|
+
import { LetopisError, fromDbError, lerr, lmsg } from './errors.js';
|
|
2
2
|
/** Транзиентные ошибки PG, которые снимает повтор: взаимная блокировка, сериализация. */
|
|
3
3
|
export function isTransient(e) {
|
|
4
4
|
const code = e.code;
|
|
@@ -17,23 +17,34 @@ export function isConnectionError(e) {
|
|
|
17
17
|
const code = String(err?.code ?? '');
|
|
18
18
|
return code.startsWith('CONNECTION_') || ['ECONNRESET', 'EPIPE', 'ECONNREFUSED', 'ETIMEDOUT', 'ECONNABORTED'].includes(code);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Откатить транзакцию на зарезервированном соединении после ошибки: после обрыва в него не пишут (postgres.js 3.4
|
|
22
|
+
* уронил бы процесс необработанным TypeError), иначе — rollback, ошибка которого не важна (план 2.0, 7.4 №7).
|
|
23
|
+
*/
|
|
24
|
+
export async function rollbackAfter(conn, err) {
|
|
25
|
+
if (isConnectionError(err))
|
|
26
|
+
return;
|
|
27
|
+
await conn.unsafe('rollback').catch(() => { });
|
|
28
|
+
}
|
|
20
29
|
/**
|
|
21
30
|
* Вернуть зарезервированное соединение в пул. После обрыва сокета пул postgres.js уже перевёл
|
|
22
31
|
* соединение в закрытые (onclose); release() вернул бы его в открытые, и следующая операция
|
|
23
32
|
* писала бы в сокет null — необработанный TypeError роняет процесс.
|
|
24
33
|
*/
|
|
25
|
-
function release(conn, err) {
|
|
34
|
+
export function release(conn, err) {
|
|
26
35
|
const e = err;
|
|
27
36
|
if (e && !e.severity && isConnectionError(e))
|
|
28
37
|
return;
|
|
29
38
|
conn.release();
|
|
30
39
|
}
|
|
31
|
-
const commitUnknown = (cause) =>
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
const commitUnknown = (cause) => lerr('write.commit_unknown', undefined, { cause });
|
|
41
|
+
/**
|
|
42
|
+
* Взаимная блокировка в явной транзакции: повторить нужно весь блок (подсказка, как hintTxRetry 0.21). Хвост —
|
|
43
|
+
* запись write.tx_retry_hint; он называет db.begin(), поэтому второй раз не дописывается.
|
|
44
|
+
*/
|
|
34
45
|
function hintTxRetry(e) {
|
|
35
46
|
if (isTransient(e) && e instanceof Error && !e.message.includes('db.begin()')) {
|
|
36
|
-
e.message += '
|
|
47
|
+
e.message += lmsg('write.tx_retry_hint');
|
|
37
48
|
}
|
|
38
49
|
return e;
|
|
39
50
|
}
|
|
@@ -147,6 +158,9 @@ const passSql = (q, ctx) => q.unsafe(`select set_config('letopis.acl', coalesce(
|
|
|
147
158
|
*/
|
|
148
159
|
export async function runTx(ctx, fn, opts = {}) {
|
|
149
160
|
const retries = opts.retries ?? RETRIES;
|
|
161
|
+
let renewed = false;
|
|
162
|
+
if (ctx.before)
|
|
163
|
+
await ctx.before(ctx);
|
|
150
164
|
for (let attempt = 1;; attempt++) {
|
|
151
165
|
let bodyDone = false;
|
|
152
166
|
let dead = false;
|
|
@@ -193,13 +207,23 @@ export async function runTx(ctx, fn, opts = {}) {
|
|
|
193
207
|
await retryDelay(attempt);
|
|
194
208
|
continue;
|
|
195
209
|
}
|
|
196
|
-
|
|
210
|
+
const le = fromDbError(e);
|
|
211
|
+
// сессия кончилась по простою: подключение по ключу выдаёт новую — повтор один раз, транзакция уже откачена
|
|
212
|
+
if (!renewed && !bodyDone && ctx.renew && isNoSession(le)) {
|
|
213
|
+
renewed = true;
|
|
214
|
+
if (await ctx.renew(ctx)) {
|
|
215
|
+
attempt--;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
throw le;
|
|
197
220
|
}
|
|
198
221
|
finally {
|
|
199
222
|
release(conn, failure);
|
|
200
223
|
}
|
|
201
224
|
}
|
|
202
225
|
}
|
|
226
|
+
const isNoSession = (e) => e instanceof LetopisError && e.code === 'no_session';
|
|
203
227
|
/** Исполнитель пула: каждая операция — своя транзакция с повторами. */
|
|
204
228
|
export function poolExecutor(ctx) {
|
|
205
229
|
return { explicit: false, run: (fn, opts) => runTx(ctx, fn, opts) };
|
|
@@ -223,7 +247,9 @@ export class ExplicitTx {
|
|
|
223
247
|
this.ctx = ctx;
|
|
224
248
|
this.conn = conn;
|
|
225
249
|
}
|
|
226
|
-
static async open(ctx) {
|
|
250
|
+
static async open(ctx, renewed = false) {
|
|
251
|
+
if (!renewed && ctx.before)
|
|
252
|
+
await ctx.before(ctx);
|
|
227
253
|
const conn = await ctx.sql.reserve();
|
|
228
254
|
try {
|
|
229
255
|
await conn.unsafe('begin isolation level read committed');
|
|
@@ -233,9 +259,13 @@ export class ExplicitTx {
|
|
|
233
259
|
ctx.passes.set(ctx, (await passSql(conn, ctx))[0]?.p);
|
|
234
260
|
}
|
|
235
261
|
catch (e) {
|
|
236
|
-
await conn
|
|
237
|
-
|
|
238
|
-
|
|
262
|
+
await rollbackAfter(conn, e);
|
|
263
|
+
release(conn, e);
|
|
264
|
+
const le = fromDbError(e);
|
|
265
|
+
// сессия кончилась по простою (как у runTx): подключение по ключу выдаёт новую — открыть ещё раз
|
|
266
|
+
if (!renewed && ctx.renew && isNoSession(le) && (await ctx.renew(ctx)))
|
|
267
|
+
return ExplicitTx.open(ctx, true);
|
|
268
|
+
throw le;
|
|
239
269
|
}
|
|
240
270
|
return new ExplicitTx(ctx, conn);
|
|
241
271
|
}
|
|
@@ -245,7 +275,7 @@ export class ExplicitTx {
|
|
|
245
275
|
}
|
|
246
276
|
async run(fn, opts) {
|
|
247
277
|
if (this.done)
|
|
248
|
-
throw
|
|
278
|
+
throw lerr('write.tx_done');
|
|
249
279
|
if (this.broken)
|
|
250
280
|
throw this.broken;
|
|
251
281
|
if (opts?.raw)
|
|
@@ -262,7 +292,7 @@ export class ExplicitTx {
|
|
|
262
292
|
/** Advisory-блокировка до конца транзакции (отдельное от блокировок семейств пространство ключей). */
|
|
263
293
|
async lock(...keys) {
|
|
264
294
|
if (this.done)
|
|
265
|
-
throw
|
|
295
|
+
throw lerr('write.lock_tx_done');
|
|
266
296
|
await this.run((q) => q.unsafe('select pg_advisory_xact_lock(hashtextextended($1, 0))', [keys.join('|')]));
|
|
267
297
|
}
|
|
268
298
|
async commit() {
|
|
@@ -294,7 +324,7 @@ export class ExplicitTx {
|
|
|
294
324
|
}));
|
|
295
325
|
changed = rows[0]?.changed ?? null;
|
|
296
326
|
if (res.command === 'ROLLBACK') {
|
|
297
|
-
this.failure =
|
|
327
|
+
this.failure = lerr('write.tx_rolled_back');
|
|
298
328
|
throw this.failure;
|
|
299
329
|
}
|
|
300
330
|
}
|