@avytheone/efmesh 0.1.0-beta.2 → 0.2.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.
@@ -5,6 +5,7 @@ import type {
5
5
  IntervalRecord,
6
6
  MigrationReport,
7
7
  PlanRecord,
8
+ RunRecord,
8
9
  SnapshotRecord,
9
10
  StateStoreShape,
10
11
  } from "./store.ts"
@@ -54,6 +55,18 @@ CREATE TABLE IF NOT EXISTS efmesh_state.intervals (
54
55
  updated_at TEXT NOT NULL,
55
56
  PRIMARY KEY (snapshot_fp, start_ts)
56
57
  );
58
+ CREATE TABLE IF NOT EXISTS efmesh_state.runs (
59
+ id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
60
+ env TEXT NOT NULL,
61
+ started_at TEXT NOT NULL,
62
+ finished_at TEXT NOT NULL,
63
+ outcome TEXT NOT NULL,
64
+ detail TEXT NOT NULL DEFAULT ''
65
+ );
66
+ CREATE TABLE IF NOT EXISTS efmesh_state.canon_cache (
67
+ key TEXT PRIMARY KEY,
68
+ canonical TEXT NOT NULL
69
+ );
57
70
  CREATE TABLE IF NOT EXISTS efmesh_state.locks (
58
71
  name TEXT PRIMARY KEY,
59
72
  acquired_at TEXT NOT NULL,
@@ -331,6 +344,42 @@ export const PostgresStateLive = (
331
344
  )) as ReadonlyArray<PlanRecord>
332
345
  }),
333
346
 
347
+ getCanon: (key) =>
348
+ attempt("getCanon", async () => {
349
+ const rows = (await sql.unsafe(
350
+ `SELECT canonical FROM efmesh_state.canon_cache WHERE key = $1`,
351
+ [key],
352
+ )) as ReadonlyArray<{ canonical: string }>
353
+ return rows[0]?.canonical
354
+ }),
355
+
356
+ putCanon: (key, canonical) =>
357
+ attempt("putCanon", async () => {
358
+ await sql.unsafe(
359
+ `INSERT INTO efmesh_state.canon_cache (key, canonical) VALUES ($1, $2)
360
+ ON CONFLICT (key) DO NOTHING`,
361
+ [key, canonical],
362
+ )
363
+ }),
364
+
365
+ recordRun: (record) =>
366
+ attempt("recordRun", async () => {
367
+ await sql.unsafe(
368
+ `INSERT INTO efmesh_state.runs (env, started_at, finished_at, outcome, detail)
369
+ VALUES ($1, $2, $3, $4, $5)`,
370
+ [record.env, record.startedAt, record.finishedAt, record.outcome, record.detail],
371
+ )
372
+ }),
373
+
374
+ listRuns: (env, limit) =>
375
+ attempt("listRuns", async () => {
376
+ return (await sql.unsafe(
377
+ `SELECT id, env, started_at AS "startedAt", finished_at AS "finishedAt", outcome, detail
378
+ FROM efmesh_state.runs WHERE env = $1 ORDER BY id DESC LIMIT $2`,
379
+ [env, limit],
380
+ )) as ReadonlyArray<RunRecord>
381
+ }),
382
+
334
383
  acquireLock: (name, ttlMs) =>
335
384
  Clock.currentTimeMillis.pipe(
336
385
  Effect.flatMap((nowMs) =>
@@ -6,6 +6,7 @@ import type {
6
6
  IntervalRecord,
7
7
  MigrationReport,
8
8
  PlanRecord,
9
+ RunRecord,
9
10
  SnapshotRecord,
10
11
  StateStoreShape,
11
12
  } from "./store.ts"
@@ -46,6 +47,18 @@ CREATE TABLE IF NOT EXISTS intervals (
46
47
  updated_at TEXT NOT NULL,
47
48
  PRIMARY KEY (snapshot_fp, start_ts)
48
49
  );
50
+ CREATE TABLE IF NOT EXISTS runs (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ env TEXT NOT NULL,
53
+ started_at TEXT NOT NULL,
54
+ finished_at TEXT NOT NULL,
55
+ outcome TEXT NOT NULL,
56
+ detail TEXT NOT NULL DEFAULT ''
57
+ );
58
+ CREATE TABLE IF NOT EXISTS canon_cache (
59
+ key TEXT PRIMARY KEY,
60
+ canonical TEXT NOT NULL
61
+ );
49
62
  CREATE TABLE IF NOT EXISTS locks (
50
63
  name TEXT PRIMARY KEY,
51
64
  acquired_at TEXT NOT NULL,
@@ -342,6 +355,40 @@ export const SqliteStateLive = (
342
355
  .all(env) as ReadonlyArray<PlanRecord>
343
356
  }),
344
357
 
358
+ getCanon: (key) =>
359
+ attempt("getCanon", () => {
360
+ const row = db
361
+ .query(`SELECT canonical FROM canon_cache WHERE key = ?1`)
362
+ .get(key) as { canonical: string } | null
363
+ return row?.canonical ?? undefined
364
+ }),
365
+
366
+ putCanon: (key, canonical) =>
367
+ attempt("putCanon", () => {
368
+ db.query(
369
+ `INSERT INTO canon_cache (key, canonical) VALUES (?1, ?2)
370
+ ON CONFLICT (key) DO NOTHING`,
371
+ ).run(key, canonical)
372
+ }),
373
+
374
+ recordRun: (record) =>
375
+ attempt("recordRun", () => {
376
+ db.query(
377
+ `INSERT INTO runs (env, started_at, finished_at, outcome, detail)
378
+ VALUES (?1, ?2, ?3, ?4, ?5)`,
379
+ ).run(record.env, record.startedAt, record.finishedAt, record.outcome, record.detail)
380
+ }),
381
+
382
+ listRuns: (env, limit) =>
383
+ attempt("listRuns", () => {
384
+ return db
385
+ .query(
386
+ `SELECT id, env, started_at AS startedAt, finished_at AS finishedAt, outcome, detail
387
+ FROM runs WHERE env = ?1 ORDER BY id DESC LIMIT ?2`,
388
+ )
389
+ .all(env, limit) as ReadonlyArray<RunRecord>
390
+ }),
391
+
345
392
  acquireLock: (name, ttlMs) =>
346
393
  Clock.currentTimeMillis.pipe(
347
394
  Effect.flatMap((nowMs) =>
@@ -10,9 +10,10 @@ export class StateError extends Data.TaggedError("StateError")<{
10
10
  * на неё; стор со схемой старше (в т.ч. созданный до появления версии)
11
11
  * открытие не проходит — данные догоняет явный `efmesh migrate`.
12
12
  * 1 — базовая раскладка (F4), 2 — applied_by в журнале планов (F5),
13
- * 3 — fingerprint_version в снапшотах (F6).
13
+ * 3 — fingerprint_version в снапшотах (F6), 4 — журнал тиков run (0.2.0),
14
+ * 5 — кэш канонизации canon_cache (0.2.0, #8).
14
15
  */
15
- export const STATE_VERSION = 3
16
+ export const STATE_VERSION = 5
16
17
 
17
18
  /** Схема стора не совпадает с ожидаемой бинарём — нужен `efmesh migrate`. */
18
19
  export class StateSchemaError extends Data.TaggedError("StateSchemaError")<{
@@ -75,6 +76,22 @@ export interface PlanRecord {
75
76
  readonly appliedBy: string
76
77
  }
77
78
 
79
+ /**
80
+ * Запись журнала тиков run (SPEC §7, issue #2): упавший в три часа ночи
81
+ * cron-тик должен быть дебажим задним числом. outcome:
82
+ * ok — тик прошёл; awaiting-human — есть неприменённые изменения (exit 2);
83
+ * lock-held — окружение занято другим процессом; error — настоящий сбой.
84
+ */
85
+ export interface RunRecord {
86
+ readonly id: number
87
+ readonly env: string
88
+ readonly startedAt: string
89
+ readonly finishedAt: string
90
+ readonly outcome: "ok" | "awaiting-human" | "lock-held" | "error"
91
+ /** ok: JSON-массив собранных моделей; awaiting-human: список изменений; error: тег ошибки. */
92
+ readonly detail: string
93
+ }
94
+
78
95
  /**
79
96
  * Учёт заполненных интервалов снапшота (SPEC §6) — единственный источник
80
97
  * правды о том, что посчитано: физическая таблица без записей здесь
@@ -137,6 +154,20 @@ export interface StateStoreShape {
137
154
  summary: string,
138
155
  appliedBy: string,
139
156
  ) => Effect.Effect<void, StateError>
157
+ /**
158
+ * Кэш канонизации (#8): ключ уже включает диалект и FINGERPRINT_VERSION —
159
+ * смена алгоритма или движка не может отдать протухший канон. Кэш — не
160
+ * данные: промах или сбой безопасны, вызывающий обязан их глотать.
161
+ */
162
+ readonly getCanon: (key: string) => Effect.Effect<string | undefined, StateError>
163
+ readonly putCanon: (key: string, canonical: string) => Effect.Effect<void, StateError>
164
+ /** Журнал тиков run (SPEC §7): исход каждого тика, включая неуспешные. */
165
+ readonly recordRun: (record: Omit<RunRecord, "id">) => Effect.Effect<void, StateError>
166
+ /** Последние тики окружения, свежие первыми. */
167
+ readonly listRuns: (
168
+ env: string,
169
+ limit: number,
170
+ ) => Effect.Effect<ReadonlyArray<RunRecord>, StateError>
140
171
  readonly listPlans: (env: string) => Effect.Effect<ReadonlyArray<PlanRecord>, StateError>
141
172
  /**
142
173
  * Межпроцессная блокировка (SPEC §7): true — получена, false — держит