letopis 1.0.0 → 1.1.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.en.md +14 -2
- package/AGENT-CHEATSHEET.md +14 -2
- package/CHANGELOG.md +186 -0
- package/README.en.md +134 -24
- package/README.md +124 -25
- package/dist/admin.d.ts +4 -0
- package/dist/admin.js +6 -0
- package/dist/cache.d.ts +33 -9
- package/dist/cache.js +81 -32
- package/dist/chain.d.ts +29 -7
- package/dist/chain.js +61 -12
- package/dist/cli.js +24 -7
- package/dist/import.d.ts +6 -0
- package/dist/import.js +196 -175
- package/dist/index.d.ts +11 -3
- package/dist/index.js +122 -18
- package/dist/load.d.ts +63 -0
- package/dist/load.js +293 -37
- package/dist/signals.d.ts +73 -0
- package/dist/signals.js +169 -0
- package/dist/sql.d.ts +1 -1
- package/dist/sql.js +17 -2
- package/dist/tx.d.ts +9 -7
- package/dist/tx.js +15 -5
- package/dist/typed.d.ts +21 -6
- package/dist/types.d.ts +76 -3
- package/dist/types.js +7 -2
- package/dist/up.d.ts +16 -2
- package/dist/up.js +34 -4
- package/dist/watch.d.ts +11 -2
- package/dist/watch.js +24 -6
- package/dist/write.d.ts +10 -1
- package/dist/write.js +32 -5
- package/llms.txt +4 -1
- package/package.json +2 -1
- package/sql/10-core.sql +8 -1
- package/sql/60-write.sql +100 -35
- package/sql/75-history.sql +358 -0
- package/sql/80-auth.sql +3 -3
- package/sql/90-time.sql +45 -9
- package/sql/95-seed.system.sql +12 -4
- package/sql/99-revision.sql +1 -1
- package/sql/README.md +1 -0
- package/sql/migrate/0006-changed-paths.sql +3 -0
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import postgres from 'postgres';
|
|
|
11
11
|
import { LetopisError, fromDbError, removed } from './errors.js';
|
|
12
12
|
import { Registry } from './registry.js';
|
|
13
13
|
import { AclPasses, ExplicitTx, Notifier, poolExecutor, runTx } from './tx.js';
|
|
14
|
+
import { checkReason } from './write.js';
|
|
14
15
|
import { makeAuth } from './auth.js';
|
|
15
16
|
import { makeAcl } from './acl.js';
|
|
16
17
|
import { idOf as idOfClass } from './uuid.js';
|
|
@@ -19,6 +20,7 @@ import { makeBatch, startEntity, startStep } from './chain.js';
|
|
|
19
20
|
import { DDL_REVISION } from './types.js';
|
|
20
21
|
import { ACL_CLASSES, ResultCache } from './cache.js';
|
|
21
22
|
import { Subscription } from './watch.js';
|
|
23
|
+
import { SIGNAL_MODES, Signals, bucket, throttle } from './signals.js';
|
|
22
24
|
import { describe as describeModel, maintain as runMaintain, rekeyClass as runRekeyClass, reset as runReset, scheduleMaintain, trimHistory as runTrim, verify as runVerify, } from './admin.js';
|
|
23
25
|
export { DDL_REVISION } from './types.js';
|
|
24
26
|
export { uuidv5, v5Name, v5Id, idOf } from './uuid.js';
|
|
@@ -64,6 +66,7 @@ function makeDb(sh, scope, exec, owner, tx) {
|
|
|
64
66
|
const txc = scope.txc;
|
|
65
67
|
const chainCtx = {
|
|
66
68
|
reg: scope.registry(), schema: sh.schema, exec, tenant: scope.tenant, owner, onQuery: sh.onQuery, slowMs: sh.slowMs,
|
|
69
|
+
reason: txc.reason ?? '', txReason: txc.reason ?? '',
|
|
67
70
|
...(sh.cache ? { cache: { store: sh.cache, who: () => ({ token: txc.token, account: txc.account, tenant: scope.tenant() }) } } : {}),
|
|
68
71
|
};
|
|
69
72
|
// реестр корня меняется после auth.switch — цепочки берут действующий
|
|
@@ -116,7 +119,10 @@ function makeDb(sh, scope, exec, owner, tx) {
|
|
|
116
119
|
const classes = o.classes ? [...new Set(o.classes.flatMap((c) => reg.family(c).map((x) => x.name)))] : null;
|
|
117
120
|
return new Subscription({
|
|
118
121
|
S: sh.S, exec: poolExecutor(txc), classes,
|
|
119
|
-
...(sh.listen ? {
|
|
122
|
+
...(sh.listen ? {
|
|
123
|
+
onSignal: (fn) => { sh.signals.add(fn); return () => void sh.signals.delete(fn); },
|
|
124
|
+
tenant: () => (sh.allTenants ? null : sh.sig.tenantToken(scope.tenant())),
|
|
125
|
+
} : {}),
|
|
120
126
|
}, o);
|
|
121
127
|
},
|
|
122
128
|
cache: {
|
|
@@ -141,22 +147,24 @@ function makeDb(sh, scope, exec, owner, tx) {
|
|
|
141
147
|
const registry = await sh.registryFor(tenant, scoped);
|
|
142
148
|
return makeDb(sh, { txc: scoped, tenant: () => tenant, registry: () => registry, root: false }, poolExecutor(scoped), nextOwner);
|
|
143
149
|
},
|
|
144
|
-
begin: async () => {
|
|
150
|
+
begin: async (o) => {
|
|
145
151
|
if (tx)
|
|
146
152
|
throw new LetopisError('invalid_query', 'вложенных транзакций нет: db.begin() внутри db.begin()');
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
// причина блока ставится в начале транзакции (setupSql) и действует на все её операции
|
|
154
|
+
const btx = o?.reason !== undefined ? { ...txc, reason: checkReason(o.reason, 'db.begin()') } : txc;
|
|
155
|
+
const t = await ExplicitTx.open(btx);
|
|
156
|
+
return makeDb(sh, btx === txc ? scope : { ...scope, txc: btx }, t, owner, t);
|
|
149
157
|
},
|
|
150
158
|
load: async (input, o) => {
|
|
151
159
|
if (tx)
|
|
152
160
|
throw new LetopisError('invalid_query', 'load() — своей транзакцией, не внутри db.begin()');
|
|
153
161
|
return load({ sql: sh.sql, schema: sh.schema, tenant: scope.tenant(), admin: sh.admin, ownerRole: sh.ownerRole }, input, o);
|
|
154
162
|
},
|
|
155
|
-
batch: (name) => {
|
|
163
|
+
batch: (name, o) => {
|
|
156
164
|
let q = batches.get(name);
|
|
157
165
|
if (!q)
|
|
158
166
|
batches.set(name, (q = []));
|
|
159
|
-
return makeBatch(chainCtx, q);
|
|
167
|
+
return makeBatch(o?.reason !== undefined ? { ...chainCtx, reason: checkReason(o.reason, 'db.batch()') } : chainCtx, q);
|
|
160
168
|
},
|
|
161
169
|
lock: async (...keys) => (tx ? tx.lock(...keys) : noTx('lock()')),
|
|
162
170
|
commit: async (tr) => (tr ? tr.commit() : tx ? tx.commit() : noTx('commit()')),
|
|
@@ -227,10 +235,20 @@ export async function connect(opts) {
|
|
|
227
235
|
throw new LetopisError('invalid_data', `connect: схемы ${opts.schema} нет — установите её: up({ schema })`);
|
|
228
236
|
if (rev < DDL_REVISION)
|
|
229
237
|
console.warn(`letopis: движок схемы ${opts.schema} ревизии ${rev}, библиотека ждёт ${DDL_REVISION} — обновите: up({ schema: '${opts.schema}', upgrade: true })`);
|
|
238
|
+
// режим сигналов установки (план 1.1, §2.2); схема, накатанная до него (нет signal_mode()), — class
|
|
239
|
+
const [sm] = await sql.unsafe(`select ${S}.signal_mode() as m`).catch((e) => {
|
|
240
|
+
if (e.code === '42883')
|
|
241
|
+
return [{ m: 'class' }];
|
|
242
|
+
throw fromDbError(e);
|
|
243
|
+
});
|
|
244
|
+
const mode = SIGNAL_MODES.includes(sm?.m) ? sm.m : 'class';
|
|
245
|
+
const sig = new Signals(mode);
|
|
230
246
|
const notifier = new Notifier(sql, opts.schema, opts.notifyIntervalMs ?? 100);
|
|
231
247
|
const passes = new AclPasses(S);
|
|
232
|
-
|
|
233
|
-
const
|
|
248
|
+
// режим none: сигналов нет — без LISTEN, кэш только по сроку (ttlOnly), подписка опрашивает журнал
|
|
249
|
+
const listen = opts.listen !== false && mode !== 'none';
|
|
250
|
+
// подключение в обход RLS видит строки всех арендаторов: его ответы сбрасывает сигнал из любого
|
|
251
|
+
const cache = opts.cache === false ? null : new ResultCache(opts.cache ?? {}, sig, Boolean(bypass));
|
|
234
252
|
const signals = new Set();
|
|
235
253
|
// продление сессии (§2.9): отдельным вызовом не чаще раза в минуту; ошибка (реплика,
|
|
236
254
|
// транзакции только для чтения) не мешает работе
|
|
@@ -241,10 +259,14 @@ export async function connect(opts) {
|
|
|
241
259
|
refreshedAt = Date.now();
|
|
242
260
|
void runTx({ sql, token, notifier: null }, (q) => q.unsafe(`select ${S}.auth_refresh()`), { retries: 0 }).catch(() => { });
|
|
243
261
|
};
|
|
244
|
-
|
|
262
|
+
// в режиме none нотификатор не отправляет: letopis.changed своей транзакции сбрасывает только свой кэш
|
|
263
|
+
const ctx = { sql, token, notifier: mode === 'none' ? null : notifier, passes, after: touch,
|
|
264
|
+
...(cache ? { onChanged: (names, raw) => cache.changed(names, raw) } : {}) };
|
|
245
265
|
const root = { tenant: null, registry: new Registry() };
|
|
246
266
|
const loadClasses = (txc) => runTx(txc, (q) => q.unsafe(`select tenant, rev, data from ${S}.entity where class = 'Class' and tenant in (${S}.ctx_tenant(), '${SYSTEM_ID}')`), { read: true });
|
|
247
267
|
const others = new Map();
|
|
268
|
+
// токены классов (режим opaque) — по именам всех реестров подключения
|
|
269
|
+
const tokens = () => sig.setClasses([root.registry, ...[...others.values()].map((o) => o.registry)].flatMap((r) => r.all().map((c) => c.name)));
|
|
248
270
|
const refresh = async () => {
|
|
249
271
|
const rows = await runTx(ctx, async (q) => {
|
|
250
272
|
const [t] = await q.unsafe(`select ${S}.ctx_tenant() as t`);
|
|
@@ -254,6 +276,7 @@ export async function connect(opts) {
|
|
|
254
276
|
root.registry.load(rows);
|
|
255
277
|
for (const o of others.values())
|
|
256
278
|
o.registry.load(await loadClasses(o.txc).catch(() => []));
|
|
279
|
+
tokens();
|
|
257
280
|
};
|
|
258
281
|
const registryFor = async (tenant, txc) => {
|
|
259
282
|
if (tenant === root.tenant)
|
|
@@ -264,37 +287,118 @@ export async function connect(opts) {
|
|
|
264
287
|
const registry = new Registry();
|
|
265
288
|
registry.load(await loadClasses(txc));
|
|
266
289
|
others.set(tenant, { registry, txc });
|
|
290
|
+
tokens();
|
|
267
291
|
return registry;
|
|
268
292
|
};
|
|
293
|
+
// ключ сигналов (режимы tenant и opaque) — сессии с правом letopis.signals; без права процесс
|
|
294
|
+
// работает без ключа: арендатора (а в opaque — и класса) в сигнале не различает
|
|
295
|
+
const fetchKey = async () => {
|
|
296
|
+
if (mode !== 'tenant' && mode !== 'opaque')
|
|
297
|
+
return;
|
|
298
|
+
const key = await runTx(ctx, (q) => q.unsafe(`select ${S}.signal_key() as k`), { read: true }).then((r) => r[0]?.k ?? null, (e) => {
|
|
299
|
+
if (e instanceof LetopisError && (e.code === 'acl_denied' || e.code === 'no_session'))
|
|
300
|
+
return null;
|
|
301
|
+
throw e;
|
|
302
|
+
});
|
|
303
|
+
// ключ сменился (reset({ all })): ответы с токенами старого ключа сигналы больше не узнают
|
|
304
|
+
if (sig.setKey(key))
|
|
305
|
+
cache?.clear();
|
|
306
|
+
};
|
|
307
|
+
await fetchKey();
|
|
269
308
|
await refresh();
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
309
|
+
// ключ, реестр и пропуска прав — заново по *, незнакомому токену, нагрузке не по режиму, а у процесса
|
|
310
|
+
// без ключа в режиме opaque — по любому сигналу; не чаще раза в 5 с: заливка канала (в том числе
|
|
311
|
+
// поддельными сигналами) не становится потоком перечитываний
|
|
312
|
+
const resync = throttle(() => {
|
|
313
|
+
passes.clear();
|
|
314
|
+
void fetchKey().then(refresh).catch(() => { });
|
|
315
|
+
}, 5000);
|
|
316
|
+
// сигнал Class — реестр сразу, как в 1.0: правка описания доходит без задержки; пока идёт перечитывание, новые
|
|
317
|
+
// сигналы сливаются в одно следующее. Три перечитывания подряд — сразу, дальше не чаще раза в 5 с: заливка
|
|
318
|
+
// канала, в том числе поддельными Class, не становится потоком перечитываний (план 1.1, §7.7 №32; ревью №1
|
|
319
|
+
// точки А)
|
|
320
|
+
let reloading = false;
|
|
321
|
+
let again = false;
|
|
322
|
+
const startReload = bucket(() => {
|
|
323
|
+
reloading = true;
|
|
324
|
+
void refresh().catch(() => { }).finally(() => {
|
|
325
|
+
reloading = false;
|
|
326
|
+
if (again) {
|
|
327
|
+
again = false;
|
|
328
|
+
reload();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}, 3, 5000);
|
|
332
|
+
const reload = () => {
|
|
333
|
+
if (reloading) {
|
|
334
|
+
again = true;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
startReload();
|
|
338
|
+
};
|
|
339
|
+
// строки прав — пропуска заново; кэш результатов и подписки — по классу и арендатору сигнала;
|
|
340
|
+
// (пере)подключение слушателя — кэш целиком, после обрыва — и ключ с реестром (сигналы потеряны)
|
|
341
|
+
const onSignal = (payload) => {
|
|
342
|
+
const s = sig.parse(payload);
|
|
343
|
+
if (s.kind === 'class' && ACL_CLASSES.has(s.cls))
|
|
274
344
|
passes.clear();
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
345
|
+
if (s.kind === 'class' && s.cls === 'Class')
|
|
346
|
+
reload();
|
|
347
|
+
else if (s.kind === 'all' || s.kind === 'opaque' || s.kind === 'unknown' || s.kind === 'foreign')
|
|
348
|
+
resync();
|
|
349
|
+
cache?.signal(s);
|
|
278
350
|
for (const fn of signals)
|
|
279
|
-
fn(
|
|
351
|
+
fn(s);
|
|
280
352
|
};
|
|
353
|
+
let listened = false;
|
|
281
354
|
const listener = listen
|
|
282
355
|
? await sql.listen(opts.schema, onSignal, () => {
|
|
356
|
+
if (listened)
|
|
357
|
+
resync();
|
|
358
|
+
listened = true;
|
|
283
359
|
cache?.listen();
|
|
284
360
|
for (const fn of signals)
|
|
285
|
-
fn('
|
|
361
|
+
fn({ kind: 'all' });
|
|
286
362
|
})
|
|
287
363
|
: null;
|
|
364
|
+
// режим none: о новых и изменённых классах сигнала нет — отпечаток классов раз в 5 с
|
|
365
|
+
let stamper = null;
|
|
366
|
+
if (mode === 'none') {
|
|
367
|
+
const stampOf = (txc) => runTx(txc, (q) => q.unsafe(`select ${S}.class_stamp() as s`), { read: true }).then((r) => String(r[0]?.s ?? ''));
|
|
368
|
+
const stamp = async () => [await stampOf(ctx), ...(await Promise.all([...others.values()].map((o) => stampOf(o.txc).catch(() => ''))))].join('|');
|
|
369
|
+
let last = await stamp();
|
|
370
|
+
let busy = false;
|
|
371
|
+
stamper = setInterval(() => {
|
|
372
|
+
if (busy)
|
|
373
|
+
return;
|
|
374
|
+
busy = true;
|
|
375
|
+
void stamp().then(async (cur) => {
|
|
376
|
+
if (cur === last)
|
|
377
|
+
return;
|
|
378
|
+
last = cur;
|
|
379
|
+
await refresh();
|
|
380
|
+
}).catch(() => { }).finally(() => {
|
|
381
|
+
busy = false;
|
|
382
|
+
});
|
|
383
|
+
}, 5000);
|
|
384
|
+
stamper.unref?.();
|
|
385
|
+
}
|
|
288
386
|
let stopMaintain = null;
|
|
289
387
|
const shared = {
|
|
290
388
|
sql, schema: opts.schema, S, notifier, passes, root, registryFor, refresh, onQuery: opts.onQuery, slowMs: opts.slowMs,
|
|
291
389
|
admin: Boolean(bypass && (role.rolsuper || role.can_set)),
|
|
292
390
|
ownerRole: owner,
|
|
293
391
|
cache,
|
|
392
|
+
sig,
|
|
393
|
+
allTenants: Boolean(bypass),
|
|
294
394
|
signals,
|
|
295
395
|
listen,
|
|
296
396
|
async close() {
|
|
297
397
|
stopMaintain?.();
|
|
398
|
+
resync.stop();
|
|
399
|
+
startReload.stop();
|
|
400
|
+
if (stamper)
|
|
401
|
+
clearInterval(stamper);
|
|
298
402
|
await notifier.close();
|
|
299
403
|
await listener?.unlisten();
|
|
300
404
|
if (own)
|
package/dist/load.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type postgres from 'postgres';
|
|
2
2
|
import { type Issue } from './errors.js';
|
|
3
|
+
import { Registry } from './registry.js';
|
|
3
4
|
type Sql = postgres.Sql<Record<string, unknown>>;
|
|
4
5
|
type Raw = Record<string, unknown>;
|
|
5
6
|
export interface LoadOptions {
|
|
@@ -27,6 +28,12 @@ export interface LoadOptions {
|
|
|
27
28
|
* existing: 'update' — прежние) значения строк; цели ссылок ему видны; владелец — по ownerDefault.
|
|
28
29
|
*/
|
|
29
30
|
as?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Первая заливка (план 1.1, §2.3): только в пустую схему (кроме строк системных классов — иначе
|
|
33
|
+
* not_empty), одной транзакцией; на время загрузки схема закрыта, индексы и триггеры сняты и
|
|
34
|
+
* перестраиваются в конце. Без commitEvery, existing (кроме 'error') и { as }.
|
|
35
|
+
*/
|
|
36
|
+
initial?: boolean;
|
|
30
37
|
}
|
|
31
38
|
export interface LoadReport {
|
|
32
39
|
/** Строк во входе. */
|
|
@@ -65,6 +72,50 @@ export interface LoadEnv {
|
|
|
65
72
|
/** Подключение может стать владельцем (set role) — иначе admin_required. */
|
|
66
73
|
admin: boolean;
|
|
67
74
|
ownerRole: string;
|
|
75
|
+
/** Сессия первой заливки (initialSession): загрузка идёт на её соединении и в её транзакции. */
|
|
76
|
+
session?: InitialState;
|
|
77
|
+
}
|
|
78
|
+
/** Внутренние настройки первой заливки — для разбивки замера (bench/initial-load.mjs); в публичные типы не входят. */
|
|
79
|
+
export interface InitialTuning {
|
|
80
|
+
/** COPY … FREEZE (по умолчанию да). */
|
|
81
|
+
freeze?: boolean;
|
|
82
|
+
/** Оставить триггеры (BEFORE ROW делает COPY построчным). */
|
|
83
|
+
keepTriggers?: boolean;
|
|
84
|
+
/** Оставить первичный ключ и уникальные ограничения. */
|
|
85
|
+
keepKeys?: boolean;
|
|
86
|
+
/** Оставить индексы GIN. */
|
|
87
|
+
keepGin?: boolean;
|
|
88
|
+
}
|
|
89
|
+
/** Проверки загрузки, отложенные до конца первой заливки: по перестроенным индексам. */
|
|
90
|
+
interface Pending {
|
|
91
|
+
reg: Registry;
|
|
92
|
+
tenant: string;
|
|
93
|
+
external: Map<string, {
|
|
94
|
+
line: number;
|
|
95
|
+
role: string;
|
|
96
|
+
allowed: string[];
|
|
97
|
+
}>;
|
|
98
|
+
owners: Map<string, number>;
|
|
99
|
+
/** verify: true — id записанных строк. */
|
|
100
|
+
verify: string[] | null;
|
|
101
|
+
}
|
|
102
|
+
/** Состояние сессии первой заливки. */
|
|
103
|
+
export interface InitialState {
|
|
104
|
+
conn: Sql;
|
|
105
|
+
/** id строк системных классов и объектов, уже записанных сессией: первичного ключа на время загрузки нет. */
|
|
106
|
+
ids: Set<string>;
|
|
107
|
+
pending: Pending[];
|
|
108
|
+
/** Арендатор → классы записанных строк: сигнал в конце сессии. */
|
|
109
|
+
touched: Map<string, Set<string>>;
|
|
110
|
+
freeze: boolean;
|
|
111
|
+
}
|
|
112
|
+
/** Первая заливка для нескольких загрузок (импорт): соединение и транзакция сессии. */
|
|
113
|
+
export interface InitialSession {
|
|
114
|
+
/** Соединение сессии (роль владельца): свои операторы импорта — в той же транзакции. */
|
|
115
|
+
readonly sql: Sql;
|
|
116
|
+
load(input: LoadInput, opts?: LoadOptions): Promise<LoadReport>;
|
|
117
|
+
/** id уже есть в схеме: строка системного класса или объект, записанный этой сессией. */
|
|
118
|
+
taken(id: string): boolean;
|
|
68
119
|
}
|
|
69
120
|
/**
|
|
70
121
|
* Загрузить строки (§2.6). Одна транзакция (или порции commitEvery): описания классов под
|
|
@@ -73,4 +124,16 @@ export interface LoadEnv {
|
|
|
73
124
|
* называет строки и нарушения.
|
|
74
125
|
*/
|
|
75
126
|
export declare function load(env: LoadEnv, input: LoadInput, opts?: LoadOptions): Promise<LoadReport>;
|
|
127
|
+
/**
|
|
128
|
+
* Первая заливка (план 1.1, §2.3): загрузки fn — одной транзакцией в пустую схему. Блокировка таблиц до
|
|
129
|
+
* проверки; в схеме только строки системных классов и записи reset (иначе not_empty); они (с seq и tx
|
|
130
|
+
* журнала) и определения ограничений, индексов и триггеров из каталога (с режимом включения tgenabled:
|
|
131
|
+
* pg_get_triggerdef его не передаёт) сохраняются в памяти; TRUNCATE; триггеры, ограничения и индексы
|
|
132
|
+
* снимаются — BEFORE ROW делает COPY построчным одним своим наличием, триггеры после оператора копят
|
|
133
|
+
* переходные таблицы; системные строки возвращаются (seq журнала — generated always: overriding system
|
|
134
|
+
* value); загрузки пишут COPY FREEZE. В конце — ограничения (дубликат найдёт сортировка), индексы,
|
|
135
|
+
* триггеры с прежним режимом, отложенные проверки загрузок, сигнал, счётчик прав и ANALYZE. Любая ошибка
|
|
136
|
+
* откатывает всё: DDL в PostgreSQL транзакционен.
|
|
137
|
+
*/
|
|
138
|
+
export declare function initialSession<T>(env: LoadEnv, fn: (s: InitialSession) => Promise<T>, tune?: InitialTuning): Promise<T>;
|
|
76
139
|
export {};
|