@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.
- package/CHANGELOG.md +136 -92
- package/README.md +61 -9
- package/README.ru.md +57 -5
- package/SPEC.md +542 -459
- package/package.json +4 -1
- package/src/cli.ts +352 -12
- package/src/efmesh.ts +2 -0
- package/src/index.ts +32 -2
- package/src/plan/categorize.ts +4 -1
- package/src/plan/diff.ts +221 -1
- package/src/plan/executor.ts +20 -0
- package/src/plan/explain.ts +114 -0
- package/src/plan/fingerprint.ts +60 -11
- package/src/plan/planner.ts +175 -6
- package/src/plan/run.ts +50 -12
- package/src/plan/schedule.ts +212 -0
- package/src/plan/status.ts +95 -0
- package/src/state/postgres.ts +49 -0
- package/src/state/sqlite.ts +47 -0
- package/src/state/store.ts +33 -2
package/src/state/postgres.ts
CHANGED
|
@@ -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) =>
|
package/src/state/sqlite.ts
CHANGED
|
@@ -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) =>
|
package/src/state/store.ts
CHANGED
|
@@ -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 =
|
|
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 — держит
|