letopis 1.2.0 → 1.4.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 +12 -7
- package/AGENT-CHEATSHEET.md +12 -7
- package/CHANGELOG.md +213 -0
- package/README.en.md +70 -21
- package/README.md +59 -18
- package/dist/index.js +27 -7
- package/dist/project.d.ts +25 -3
- package/dist/project.js +122 -21
- package/dist/sync.js +16 -1
- package/dist/up.d.ts +17 -0
- package/dist/up.js +64 -0
- package/dist/watch.js +28 -3
- package/llms.txt +1 -1
- package/package.json +2 -1
- package/sql/10-core.sql +3 -1
- package/sql/50-acl.sql +5 -1
- package/sql/60-write.sql +166 -25
- package/sql/88-derived.sql +83 -3
- package/sql/90-time.sql +113 -22
- package/sql/99-revision.sql +3 -2
- package/sql/README.md +3 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# letopis 1.
|
|
1
|
+
# letopis 1.4
|
|
2
2
|
|
|
3
3
|
**letopis** — библиотека для Node.js, хранилище версионированных сущностей на PostgreSQL 18. Каждое изменение
|
|
4
4
|
сохраняется версией в журнале. Поэтому у любого объекта есть история, состояние на любую дату и подписка на
|
|
@@ -362,12 +362,23 @@ const adm = await connect({ dsn: ADMIN, schema: r.schema, token: r.tenant!.token
|
|
|
362
362
|
получит `upgrade: true`. `up({ dsn, schema, upgrade: true })` применяет миграции из `sql/migrate/` и повторно
|
|
363
363
|
накатывает функции; данные сохраняются.
|
|
364
364
|
|
|
365
|
+
Ревизия меняется не с каждой версией: у 1.1–1.4 она одна, а функции разные. Поэтому `up()` пишет в комментарий схемы
|
|
366
|
+
ещё и штамп движка: версию пакета и хэш его файлов SQL. Если штамп не совпадает с файлами пакета, `connect()`
|
|
367
|
+
предупреждает. Когда схему накатила более старая версия (или версия до 1.4, у которой штампа нет), он советует
|
|
368
|
+
`up({ schema })`. Когда схему уже обновила более новая версия, он советует обновить пакет: `up()` старой версией вернул
|
|
369
|
+
бы прежние функции. Новая версия пакета без правок SQL штамп не меняет, и предупреждения нет.
|
|
370
|
+
|
|
365
371
|
Первый `up()` после перехода на 1.2 строит индекс журнала `log_class_tenant_at (class, tenant, at)` вместо
|
|
366
372
|
`log_class_at`: срез `asOf` и `withDeleted` по классу больше не читает журнал других арендаторов с тем же именем
|
|
367
373
|
класса. Пока индекс строится, запись в журнал ждёт. На большой установке постройте его заранее, не останавливая
|
|
368
374
|
записи: `create index concurrently log_class_tenant_at on "v2.<схема>".log (class, tenant, at)` от роли владельца, —
|
|
369
375
|
тогда `up()` только снимет прежний.
|
|
370
376
|
|
|
377
|
+
Обновление схем 1.1.0 и 1.0.1 проверяет тест `upgrade`. Схему ставит прежний пакет, данные пишет прежний движок, затем
|
|
378
|
+
вызывается `up()` текущей версии. После него версии и хэши прежнего движка на месте, `verify({ data: true })` чист, а
|
|
379
|
+
каталог схемы совпадает с чистой установкой. Схема 1.1.0 обновляется простым `up()`: ревизия у неё та же. Схема 1.0.1
|
|
380
|
+
обновляется через `up({ upgrade: true })`.
|
|
381
|
+
|
|
371
382
|
### Разработка без PostgreSQL: PGlite
|
|
372
383
|
|
|
373
384
|
`up({ pglite: true })` поднимает PGlite (PostgreSQL 18 на WebAssembly) с pgcrypto и сокетом на свободном порту.
|
|
@@ -544,11 +555,19 @@ const Movement = link('Movement', { amount: z.number().int() }, { ends: { Wallet
|
|
|
544
555
|
достаточно права на запись в источник.
|
|
545
556
|
- Описание проверяет база (`invalid_class`): поле есть в схеме, оно числовое и не входит в ключ; конец `via` —
|
|
546
557
|
одиночный и принимает класс цели; поле `sum` — число; агрегаты не образуют цикл; цель и источник — не системные и
|
|
547
|
-
не производные классы.
|
|
558
|
+
не производные классы. Класс-источник описывают раньше агрегата: агрегат над классом, которого ещё нет, отклоняется.
|
|
559
|
+
У класса арендатора источник ищут среди классов арендатора. `syncModels` сам описывает источники раньше целей, а
|
|
560
|
+
модели по кругу (агрегат над собственным потомком, цикл агрегатов) отклоняет до записи (`invalid_class` с кругом).
|
|
561
|
+
Класс `of` не удаляется, пока его называет агрегат класса того же арендатора: сначала агрегат убирают из описания цели.
|
|
548
562
|
- Строки попадают в агрегат только с ведома того, кто читает их все: поле считает и строки, которые автору не видны.
|
|
549
563
|
Описать или изменить агрегат может только тот, кто без условий читает все строки источника и его потомков. Сделать
|
|
550
|
-
класс источником (новый
|
|
564
|
+
класс источником (новый потомок источника, смена `parent`) может только тот, кто без условий читает все
|
|
551
565
|
строки этого класса и его потомков — и до правки, и после. Иначе `acl_denied`.
|
|
566
|
+
- Новый источник проверяется и по тому, кто описал агрегат: он тоже должен без условий читать все строки класса.
|
|
567
|
+
Описавший — автор версии строки `Class` цели, с которой описание агрегата стало нынешним. Если он их не читает, класс
|
|
568
|
+
источником не станет (`acl_denied`), пока агрегат не опишет заново тот, кто читает. У агрегата класса System
|
|
569
|
+
описавший проверяется по классам System, как и при описании. Класс арендатора под агрегатом System проверяется
|
|
570
|
+
только у того, кто его заводит.
|
|
552
571
|
- Опция наследуется, потомок заменяет её целиком. Добавить или изменить агрегат — ужесточение: запись в цель и в
|
|
553
572
|
источники ждёт, пока поле пересчитывается у всех строк.
|
|
554
573
|
- `db.rebuildAggregates(класс)` пересчитывает поле из текущих строк — для ремонта. Версия пишется только там, где
|
|
@@ -955,7 +974,8 @@ await db.Customer(temp).purge({ confirm: true }).rows() // история
|
|
|
955
974
|
версии 1 остаётся запись `purge` с числом стёртых версий. История системных классов не стирается. Пока проектор,
|
|
956
975
|
чьи источники `from` включают класс объекта ([«Проекторы»](#проекторы)), не прочёл удаление, `purge` отказывает
|
|
957
976
|
(`projector_pending`): стёртое надгробие проектор не увидел бы, и вклад объекта остался бы в итогах. Удаление и
|
|
958
|
-
`purge` одной транзакцией в таком классе поэтому не проходят.
|
|
977
|
+
`purge` одной транзакцией в таком классе поэтому не проходят. Так же `db.trimHistory` отказывает, пока проектор не
|
|
978
|
+
прочёл первую оставшуюся версию: прошлую версию для `before` он бы уже не нашёл. Удалить аккаунт — `db.auth.purgeAccount`
|
|
959
979
|
([раздел 10](#10-пользователи-вход-и-права)).
|
|
960
980
|
|
|
961
981
|
## 8. Транзакции, батчи и деньги
|
|
@@ -1423,15 +1443,21 @@ await anna.auth.switch(SHOP) // арендатор сессии
|
|
|
1423
1443
|
для одного класса, действовало бы и на строки другого. Нарушение — `invalid_class`.
|
|
1424
1444
|
|
|
1425
1445
|
По тем же именам правила находят и старые версии строк в журнале. Поэтому класс удаляется, только когда у него нет
|
|
1426
|
-
строк, потомков и истории строк: историю удалённых строк сначала стирает `purge()`.
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1446
|
+
строк, потомков и истории строк: историю удалённых строк сначала стирает `purge()`. Для класса System не в счёт
|
|
1447
|
+
строки, потомки и история арендатора, у которого есть свой класс с тем же именем: они относятся к его классу. Класс,
|
|
1448
|
+
удалённый с историей раньше, создаётся заново только с прежними `parent`, `alias`, `derivedBy` и `history` — иначе
|
|
1449
|
+
`invalid_class`: старые версии его строк попали бы под чужие правила или другое прореживание. Правила находят старые
|
|
1450
|
+
версии и по алиасам предков. Поэтому, пока удалённые потомки класса хранят историю строк, класс не меняет алиас
|
|
1451
|
+
(`invalid_class`), а удалённым создаётся заново только с прежним описанием. Если журнал хранит строки класса с этим
|
|
1452
|
+
именем, но не его надгробие (например, строки класса System, удалённого в 1.1), класс с этим именем не создаётся, пока
|
|
1453
|
+
историю не сотрёт `purge()`.
|
|
1430
1454
|
|
|
1431
1455
|
Производный класс (`derivedBy`) заводит, меняет и удаляет только администратор: администраторское подключение или
|
|
1432
1456
|
аккаунт с правом System `letopis.projector` (у системного администратора оно есть по маске `letopis.*`). Проектор
|
|
1433
1457
|
пишет в такой класс итоги по всем строкам источников арендатора, без прав чтения, поэтому пользователь, заведя его
|
|
1434
|
-
сам, читал бы итоги по строкам, которые ему читать запрещено.
|
|
1458
|
+
сам, читал бы итоги по строкам, которые ему читать запрещено. Класс, у которого есть производные потомки, тоже
|
|
1459
|
+
правит только администратор: правка предка меняет описание потомка (поля, ключ, `history`). Пометку `aggregatedBy`
|
|
1460
|
+
(класс стал источником агрегата) база ставит и производному классу сама: полей и ключа она не меняет.
|
|
1435
1461
|
|
|
1436
1462
|
### Выдать право
|
|
1437
1463
|
|
|
@@ -1656,7 +1682,9 @@ for await (const ev of sub) { // ev: { cursor, row, id, class, op, r
|
|
|
1656
1682
|
запись после него в поток придёт. Пример выше читает с `'start'`.
|
|
1657
1683
|
- Вызовы `sub.next()` идут по очереди: параллельные получают разные события. Вызов, брошенный по таймауту
|
|
1658
1684
|
(`Promise.race`), всё равно получит своё событие — держите его обещание и ждите его снова.
|
|
1659
|
-
- `classes` — только эти классы с потомками; `pollMs` — запасной опрос (1000 мс).
|
|
1685
|
+
- `classes` — только эти классы с потомками; `pollMs` — запасной опрос (1000 мс). Строка, ушедшая из этих классов
|
|
1686
|
+
сменой класса без смены id, приходит событием удаления в прежнем классе (`op: 'delete'`, `row` — прежняя версия с
|
|
1687
|
+
`$deleted`), даже если новый класс подписчику не виден. Событие приходит, если прежнюю версию подписчик видел.
|
|
1660
1688
|
- События идут без потерь и повторов, под правами читателя, в порядке номеров транзакций, а внутри транзакции — в
|
|
1661
1689
|
порядке записи. Это не порядок фиксации: версии одного объекта сверяйте по `rev`.
|
|
1662
1690
|
- Долгая пишущая транзакция в любой базе кластера задерживает поток: `await sub.lag()` показывает отставание в мс.
|
|
@@ -1772,7 +1800,8 @@ sales.close() // stop() после текущ
|
|
|
1772
1800
|
пачками, каждая своей транзакцией со своим снимком, пока хвост журнала идёт для всех: изменения арендатора выше
|
|
1773
1801
|
последней пачки хвост пропускает (их прочтёт пачка), в пройденной — если транзакция видна в её снимке. Производные
|
|
1774
1802
|
строки арендатора удаляются сразу и до конца перестройки неполны (`status().rebuildingTenants`); блокировку,
|
|
1775
|
-
занятую `run()` другого процесса, перестройка пережидает.
|
|
1803
|
+
занятую `run()` другого процесса, перестройка пережидает. Прерванную перестройку (процесс остановился) продолжает
|
|
1804
|
+
`run()` любого процесса — с `onExpired` и без него.
|
|
1776
1805
|
- `status().state` описывает проекцию в базе, а не процесс: `'new'` — проекции ещё нет, её построит первый `run()`
|
|
1777
1806
|
или `rebuild()`; `'running'` — проекция построена и ведётся по журналу (это не значит, что какой-то процесс сейчас
|
|
1778
1807
|
выполняет `run()`); `'rebuilding'` — идёт или прервана перестройка пачками.
|
|
@@ -1798,6 +1827,15 @@ sales.close() // stop() после текущ
|
|
|
1798
1827
|
его производных строк проектор не строит. Включите журнал класса и перестройте арендатора `rebuild({ tenant })`.
|
|
1799
1828
|
- `batch` — сколько изменений в пачке (500), `pollMs` — запасной опрос (1000 мс): сигналы источников будят
|
|
1800
1829
|
проектор раньше.
|
|
1830
|
+
- `lockTimeout` — сколько транзакция проектора ждёт чужую блокировку строки (5000 мс; 0 — без предела). Транзакция
|
|
1831
|
+
любого арендатора, которая держит строку итога или цель конца, иначе держала бы пачку всех арендаторов. Дольше —
|
|
1832
|
+
пачка откатывается, курсор остаётся, `run()` не отклоняется, а пропускает проход (`skipped`) и повторяет его. Отчёт
|
|
1833
|
+
называет арендатора, на чьей строке встала пачка (`locked`). `rebuild({ tenant, chunked: true })` такую блокировку
|
|
1834
|
+
пережидает.
|
|
1835
|
+
- `lockRetries` — сколько проходов подряд запись одного арендатора может упираться в `lockTimeout`, прежде чем он
|
|
1836
|
+
выбудет (по умолчанию 0 — не выбывает). Выбывший получает код `55P03`, и пачка идёт для остальных; его изменения
|
|
1837
|
+
пропускаются до `rebuild({ tenant })`. Счёт ведёт процесс, удачная запись арендатора его сбрасывает. Учтите: долгая
|
|
1838
|
+
исключительная блокировка семейства класса System (ужесточение описания) упрётся в каждого арендатора по очереди.
|
|
1801
1839
|
- Каждая правка, которую `out` отправляет в базу, — отдельная правка строки, и скорость проектора задаёт число
|
|
1802
1840
|
правок, а не число изменений. Свёртка `inc` сводит правки строки к одной за пачку, поэтому handle из примера выше
|
|
1803
1841
|
(`out.inc` на каждое изменение) на машине замеров догонял 6 тысяч изменений в секунду при `batch` 500 и 33 тысячи
|
|
@@ -2238,12 +2276,12 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2238
2276
|
|
|
2239
2277
|
| Член | Что делает |
|
|
2240
2278
|
|---|---|
|
|
2241
|
-
| опции `ProjectorOptions` | `from`, `into` — классы с потомками; `version` — целое от 1; `handle(changes, out)`; `withBefore`; `onExpired: 'rebuild' \| 'error'` (`'rebuild'` — пачками); `batch` — изменений в пачке (500, от 1 до 10 000); `pollMs` — запасной опрос (1000 мс); `tenants` — id арендаторов-участников |
|
|
2279
|
+
| опции `ProjectorOptions` | `from`, `into` — классы с потомками; `version` — целое от 1; `handle(changes, out)`; `withBefore`; `onExpired: 'rebuild' \| 'error'` (`'rebuild'` — пачками); `batch` — изменений в пачке (500, от 1 до 10 000); `pollMs` — запасной опрос (1000 мс); `lockTimeout` — ожидание чужой блокировки строки (5000 мс, 0 — без предела); `lockRetries` — проходов подряд до выбывания арендатора с чужой блокировкой (0 — не выбывает); `tenants` — id арендаторов-участников |
|
|
2242
2280
|
| `run({ once })` | догнать журнал (`once`) или следовать за ним до `stop()` → `ProjectorReport` |
|
|
2243
2281
|
| `rebuild({ chunked, tenant })` | перестроить с текущего состояния: атомарно, пачками или одного арендатора → `ProjectorReport` |
|
|
2244
2282
|
| `status()` | → `ProjectorStatus`: `{ name, state, version, cursor, at, lag, failed, rebuildingTenants }`; `failed` — `{ [id арендатора]: ProjectorFailure }`, где `ProjectorFailure` — `{ cursor, code, message }` |
|
|
2245
2283
|
| `stop()`, `close()` | остановить `run()` после текущей пачки; то же и отписаться от сигналов |
|
|
2246
|
-
| `ProjectorReport` | `{ batches, changes, created, updated, deleted, unchanged, skipped, rebuilds }` |
|
|
2284
|
+
| `ProjectorReport` | `{ batches, changes, created, updated, deleted, unchanged, skipped, rebuilds, locked }` |
|
|
2247
2285
|
| `ProjectorChange` | `{ cursor, id, class, tenant, op, rev, at, author, row }` и, если есть, `agent`, `reason`, `moved`, `before`, `rebuild` |
|
|
2248
2286
|
| `ProjectorOut` | `upsert(класс, строка, { tenant })`, `inc(класс, строка, приращения, { tenant })`, `delete(класс, ключ или id, { tenant })`, `get(класс, ключ или id, { tenant })` → `Row \| null`; строка — `ProjectorRow` в формате загрузчика |
|
|
2249
2287
|
|
|
@@ -2304,7 +2342,7 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2304
2342
|
| `confirm_mismatch` | LT020 | подтверждение сброса не совпадает с именем схемы |
|
|
2305
2343
|
| `derived_class` | LT021 | строку производного класса (`derivedBy`) пишет, правит или удаляет не его проектор; конец ведёт на такую строку или на производный класс; проектор пишет в класс вне своих `into` или не нашёл ни одного арендатора, у которого все классы `into` помечены его `derivedBy` |
|
|
2306
2344
|
| `aggregate_field` | LT022 | приложение передало или изменило агрегатное поле (`aggregate`): его считает база |
|
|
2307
|
-
| `projector_pending` | LT023 | `purge` удалённого объекта, чьё удаление ещё не прочёл проектор его
|
|
2345
|
+
| `projector_pending` | LT023 | `purge` удалённого объекта, чьё удаление ещё не прочёл проектор его класса, или `trimHistory`, чью первую оставшуюся версию он ещё не прочёл: сначала `run()` проектора |
|
|
2308
2346
|
|
|
2309
2347
|
Коды библиотеки:
|
|
2310
2348
|
|
|
@@ -2388,7 +2426,7 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2388
2426
|
|
|
2389
2427
|
## 18. Ограничения
|
|
2390
2428
|
|
|
2391
|
-
Здесь собрано то, что letopis 1.
|
|
2429
|
+
Здесь собрано то, что letopis 1.4 сознательно не делает или делает с оговорками, и где за поведением нужно следить.
|
|
2392
2430
|
|
|
2393
2431
|
**Платформа**
|
|
2394
2432
|
|
|
@@ -2468,8 +2506,9 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2468
2506
|
- Счётчики попыток OTP и TOTP откатываются вместе с транзакцией вызывающего: частоту попыток ограничивает сервис.
|
|
2469
2507
|
- Самостоятельная привязка занятого идентификатора отвечает `acl_denied` — пользователь с недавним входом так
|
|
2470
2508
|
узнаёт, что идентификатор занят. Изменения прав во всех арендаторах упорядочены одной строкой счётчика.
|
|
2471
|
-
-
|
|
2472
|
-
|
|
2509
|
+
- Тексты отказов называют класс, роль и id ссылающейся строки, только если актор её видит: каскад удаления, снятие
|
|
2510
|
+
конца, перевод ссылок при смене класса и ключа, конфликт смены `parent` (`tightening_conflict`). Число невидимых строк, мешающих удалению или смене класса
|
|
2511
|
+
(`reclass_denied`), и существование аккаунта по id (как цели ссылки) не скрываются.
|
|
2473
2512
|
- Ужесточение описания класса и `rekeyClass` проверяют все строки семейства, поэтому требуют права `READ` без условий
|
|
2474
2513
|
на все строки класса и его потомков: иначе пробное ужесточение (`maximum`, `enum`, `pattern`) отвечало бы числом
|
|
2475
2514
|
невидимых строк, которые ему не соответствуют. Системный администратор, ужесточая класс System, получает число
|
|
@@ -2547,6 +2586,7 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2547
2586
|
- `purge()` ждёт проекторов: пока проектор, чьи источники включают класс объекта, а у арендатора объекта есть его
|
|
2548
2587
|
производный класс, не прочёл удаление, `purge` отвечает `projector_pending`. Проектор, который больше не запускают,
|
|
2549
2588
|
держит `purge` своих источников, пока у арендатора есть его производные классы: запустите его или удалите их.
|
|
2589
|
+
Так же ждёт `trimHistory`: пока проектор не прочёл первую оставшуюся версию, обрезка отвечает `projector_pending`.
|
|
2550
2590
|
|
|
2551
2591
|
**Агрегаты в базе**
|
|
2552
2592
|
|
|
@@ -2613,7 +2653,7 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2613
2653
|
# hash — цепочка хэшей версий, подмена и разрыв журнала, verify() (этап 1)
|
|
2614
2654
|
# harness — тестовая обвязка: подъём базы, схема на файл, две строки подключения (этап 0)
|
|
2615
2655
|
# jsonschema — компиляция JSON Schema в jsonpath и проверка данных в базе (этап 1)
|
|
2616
|
-
# impersonation — имперсонация: letopis.account только для сервиса с правом, запреты целей, author и agent в журнале, sessionFor; реестр db.as() — один на арендатора, не пустеет, перечитывается по сигналу своего арендатора (этап 5; доработки после точки Б)
|
|
2656
|
+
# impersonation — имперсонация: letopis.account только для сервиса с правом, запреты целей, author и agent в журнале, sessionFor; реестр db.as() — один на арендатора, не пустеет, перечитывается по сигналу своего арендатора (этап 5; доработки после точки Б); реестр фасада не общий с корнем, пустой реестр не кэшируется (хвост ревью безопасности №3)
|
|
2617
2657
|
# install — установщик и роли: повторная установка, две установки в базе, три таблицы (этап 1)
|
|
2618
2658
|
# member — членство: работа в арендаторе по ролям, auth.switch, выход с переходом владения, системные строки, роли арендатора без прав System (этап 5)
|
|
2619
2659
|
# model — модели на Zod, нормализатор JSON Schema, перевод шаблонов, совпадение с метасхемой базы (этап 2)
|
|
@@ -2637,6 +2677,7 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2637
2677
|
# cache — кэш результатов: попадание, свои и чужие записи, ключ с пользователем, права, погашенные сессии, обрыв слушателя, ttl, пометка класса, без LISTEN, гонка, db.sql, предел счётчиков сигналов (этап 6; план 1.1, точка А)
|
|
2638
2678
|
# maintain — обслуживание: истёкшие сессии и коды, права, один процесс под блокировкой, расписание (этап 6)
|
|
2639
2679
|
# up — полный установщик: свежая установка, повторный накат, свои сиды, автосоздание базы, UTF8, версия 17, upgrade с миграциями, PGlite, первый арендатор и пользователь (up({ tenant }), createTenant) (этапы 6, 8)
|
|
2680
|
+
# upgrade — обновление схемы с 1.1.0 (повторный накат) и с 1.0.1 (upgrade: true, миграция 0006) пакетами letopis-11 и letopis-1: версии и хэши прежнего движка, verify(), каталог против чистой установки, строки-замки, приложение, подписка и проектор по старым данным, повторный up() (доработки после 1.2.0, этап 1)
|
|
2640
2681
|
# scenario-salon — сквозной сценарий салона: 21 акт 0.21 (salon) с матрицей покрытия API 1.0 (этап 6)
|
|
2641
2682
|
# telemetry — onQuery и slowMs, describe() и метаданные классов с наследованием, describeText (этап 6)
|
|
2642
2683
|
# history — diff и blame по журналу, причина изменения у глаголов, db.begin() и батча; версия, скрытая условием ACL, колонка changed, пропуск версии, row_changes на 20 тыс. целей (план 1.1, этап 1 и точка А)
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { Subscription } from './watch.js';
|
|
|
24
24
|
import { Projector } from './project.js';
|
|
25
25
|
import { SIGNAL_MODES, Signals, bucket, throttle } from './signals.js';
|
|
26
26
|
import { describe as describeModel, maintain as runMaintain, rebuildAggregates as runRebuildAggregates, rekeyClass as runRekeyClass, reset as runReset, scheduleMaintain, trimHistory as runTrim, verify as runVerify, } from './admin.js';
|
|
27
|
+
import { stampDrift } from './up.js';
|
|
27
28
|
export { DDL_REVISION } from './types.js';
|
|
28
29
|
export { uuidv5, v5Name, v5Id, idOf } from './uuid.js';
|
|
29
30
|
export { LetopisError, ValidationError, fromDbError } from './errors.js';
|
|
@@ -142,12 +143,22 @@ function makeDb(sh, scope, exec, owner, tx) {
|
|
|
142
143
|
}
|
|
143
144
|
// имперсонация и арендатор проверяются базой сразу: ошибка — здесь, а не на первой цепочке
|
|
144
145
|
const scoped = { ...txc, account: id ?? txc.account, tenant: o?.tenant ?? (id === undefined ? txc.tenant : undefined) };
|
|
145
|
-
|
|
146
|
+
let [r] = await runTx(scoped, (q) => q.unsafe(`select account, tenant from ${sh.S}.ctx()`), { read: true });
|
|
146
147
|
if (!r?.account)
|
|
147
148
|
throw new LetopisError('no_session', 'db.as(): нет действующей сессии');
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
// контекст сессии между запросами ушёл из арендатора (сменился или кончился): реестр не загружен и не
|
|
150
|
+
// кэширован (хвост ревью безопасности №3) — контекст читается заново, не больше трёх раз
|
|
151
|
+
for (let attempt = 1;; attempt++) {
|
|
152
|
+
const tenant = r.tenant;
|
|
153
|
+
const registry = await sh.registryFor(tenant, scoped);
|
|
154
|
+
if (registry)
|
|
155
|
+
return makeDb(sh, { txc: scoped, tenant: () => tenant, registry: () => registry, root: false }, poolExecutor(scoped), nextOwner);
|
|
156
|
+
if (attempt === 3)
|
|
157
|
+
throw new LetopisError('no_session', 'db.as(): контекст сессии меняется — реестр арендатора не загружен');
|
|
158
|
+
[r] = await runTx(scoped, (q) => q.unsafe(`select account, tenant from ${sh.S}.ctx()`), { read: true });
|
|
159
|
+
if (!r?.account)
|
|
160
|
+
throw new LetopisError('no_session', 'db.as(): нет действующей сессии');
|
|
161
|
+
}
|
|
151
162
|
},
|
|
152
163
|
begin: async (o) => {
|
|
153
164
|
if (tx)
|
|
@@ -257,6 +268,12 @@ export async function connect(opts) {
|
|
|
257
268
|
throw new LetopisError('invalid_data', `connect: схемы ${opts.schema} нет — установите её: up({ schema })`);
|
|
258
269
|
if (rev < DDL_REVISION)
|
|
259
270
|
console.warn(`letopis: движок схемы ${opts.schema} ревизии ${rev}, библиотека ждёт ${DDL_REVISION} — обновите: up({ schema: '${opts.schema}', upgrade: true })`);
|
|
271
|
+
else if (rev === DDL_REVISION) {
|
|
272
|
+
// ревизия та же, а функции схемы ставила другая версия движка — штамп в комментарии схемы (99-revision.sql)
|
|
273
|
+
const drift = await stampDrift(opts.schema, String(rv.c ?? ''));
|
|
274
|
+
if (drift)
|
|
275
|
+
console.warn(drift);
|
|
276
|
+
}
|
|
260
277
|
// режим сигналов установки (план 1.1, §2.2); схема, накатанная до него (нет signal_mode()), — class
|
|
261
278
|
const [sm] = await sql.unsafe(`select ${S}.signal_mode() as m`).catch((e) => {
|
|
262
279
|
if (e.code === '42883')
|
|
@@ -315,9 +332,9 @@ export async function connect(opts) {
|
|
|
315
332
|
}
|
|
316
333
|
tokens();
|
|
317
334
|
};
|
|
335
|
+
// фасад db.as() не делит реестр с корнем, даже в том же арендаторе: корень после auth.switch() перечитывает свой
|
|
336
|
+
// реестр классами нового арендатора (хвост ревью безопасности №3)
|
|
318
337
|
const registryFor = async (tenant, txc) => {
|
|
319
|
-
if (tenant === root.tenant)
|
|
320
|
-
return root.registry;
|
|
321
338
|
const known = others.get(tenant);
|
|
322
339
|
if (known) {
|
|
323
340
|
known.txc = txc;
|
|
@@ -327,8 +344,11 @@ export async function connect(opts) {
|
|
|
327
344
|
let pending = loading.get(tenant);
|
|
328
345
|
if (!pending) {
|
|
329
346
|
pending = (async () => {
|
|
347
|
+
const rows = await loadClasses(txc, tenant);
|
|
348
|
+
if (!rows)
|
|
349
|
+
return null;
|
|
330
350
|
const registry = new Registry();
|
|
331
|
-
registry.load(
|
|
351
|
+
registry.load(rows);
|
|
332
352
|
others.set(tenant, { registry, txc });
|
|
333
353
|
tokens();
|
|
334
354
|
return registry;
|
package/dist/project.d.ts
CHANGED
|
@@ -98,6 +98,17 @@ export interface ProjectorOptions {
|
|
|
98
98
|
batch?: number;
|
|
99
99
|
/** Запасной опрос журнала, мс (по умолчанию 1000; сигналы источников будят раньше). */
|
|
100
100
|
pollMs?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Сколько транзакция проектора ждёт чужую блокировку строки, мс (по умолчанию 5000; 0 — без предела). Дольше —
|
|
103
|
+
* пачка откатывается, курсор остаётся, run() не отклоняется, а пропускает проход и повторяет его.
|
|
104
|
+
*/
|
|
105
|
+
lockTimeout?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Сколько проходов подряд запись одного арендатора может упираться в lockTimeout, прежде чем он выбудет (код 55P03,
|
|
108
|
+
* дальше — rebuild({ tenant })), а пачка пойдёт для остальных; по умолчанию 0 — не выбывает, проход повторяется для всех.
|
|
109
|
+
* Счёт — в процессе, удачная запись арендатора его сбрасывает.
|
|
110
|
+
*/
|
|
111
|
+
lockRetries?: number;
|
|
101
112
|
/**
|
|
102
113
|
* Явный список арендаторов проектора: участники — только они (и только те, у кого каждый класс into помечен
|
|
103
114
|
* derivedBy). Без списка участник — любой арендатор с такими классами.
|
|
@@ -113,10 +124,12 @@ export interface ProjectorReport {
|
|
|
113
124
|
updated: number;
|
|
114
125
|
deleted: number;
|
|
115
126
|
unchanged: number;
|
|
116
|
-
/** Пачек, пропущенных из-за блокировки: проектор работает в другом
|
|
127
|
+
/** Пачек, пропущенных из-за блокировки: проектор работает в другом процессе или строку держит чужая транзакция. */
|
|
117
128
|
skipped: number;
|
|
118
129
|
/** Перестроек. */
|
|
119
130
|
rebuilds: number;
|
|
131
|
+
/** Арендаторы, чья запись упиралась в чужую блокировку строки дольше lockTimeout (по порядку, без повторов). */
|
|
132
|
+
locked: string[];
|
|
120
133
|
}
|
|
121
134
|
/** Выбывший арендатор: база отклонила запись его результатов. */
|
|
122
135
|
export interface ProjectorFailure {
|
|
@@ -168,12 +181,18 @@ export declare class Projector {
|
|
|
168
181
|
/** Источники арендатора без журнала (history: false): их изменений проектор не увидит — арендатор выбывает. */
|
|
169
182
|
private blind;
|
|
170
183
|
private stale;
|
|
184
|
+
/** В чекпоинте есть начатая перестройка арендатора пачками (по последней пачке; до первой — проверить). */
|
|
185
|
+
private tenantsPending;
|
|
171
186
|
private running;
|
|
172
187
|
private stopped;
|
|
173
188
|
private wake;
|
|
174
189
|
private off;
|
|
175
190
|
private readonly batch;
|
|
176
191
|
private readonly pollMs;
|
|
192
|
+
private readonly lockMs;
|
|
193
|
+
private readonly lockRetries;
|
|
194
|
+
/** Проходов подряд, в которых запись арендатора упиралась в lockTimeout (lockRetries). */
|
|
195
|
+
private readonly lockHits;
|
|
177
196
|
private readonly only;
|
|
178
197
|
constructor(env: ProjectorEnv, name: string, def: ProjectorOptions);
|
|
179
198
|
/**
|
|
@@ -299,10 +318,13 @@ export declare class Projector {
|
|
|
299
318
|
*/
|
|
300
319
|
private tenantChunk;
|
|
301
320
|
/**
|
|
302
|
-
*
|
|
303
|
-
*
|
|
321
|
+
* Перестройка арендаторов пачками — проход run(): с start (onExpired: 'rebuild') начинает её у каждого выбывшего по
|
|
322
|
+
* cursor_expired, затем продвигает на пачку каждую начатую — автоматическую и прерванную rebuild({ tenant, chunked:
|
|
323
|
+
* true }). true — работа ещё осталась.
|
|
304
324
|
*/
|
|
305
325
|
private tenantWork;
|
|
326
|
+
/** Повторять fn, пока строку держит чужая транзакция дольше lockTimeout (55P03): пауза 20 мс, проход — в skipped. */
|
|
327
|
+
private untilFree;
|
|
306
328
|
/** Ждать сигнал источников, остановку или запасной опрос. */
|
|
307
329
|
private sleep;
|
|
308
330
|
}
|