@valbuild/server 0.102.0 → 0.103.1

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.
@@ -0,0 +1,216 @@
1
+ import { ModuleFilePath, PatchId } from "@valbuild/core";
2
+ import { z } from "zod";
3
+ import type { AuthorId, BaseSha } from "./ValOps.js";
4
+ import { PatchLogEntry, PatchLogProblem } from "./patchLog.js";
5
+ /**
6
+ * The on-disk shape of the local-dev patch store.
7
+ *
8
+ * ```
9
+ * .val/patches/
10
+ * patches.log the order, and the only place it lives
11
+ * patches.repair.log what repair has done, for the person who has to know
12
+ * <patchId>/patch.json one plain, self-contained directory per patch
13
+ * <patchId>/base.json written when the patch is published
14
+ * <patchId>/files/… binary payloads for this patch's file ops
15
+ * ```
16
+ *
17
+ * A directory is named after the patch it holds, and a record references nothing
18
+ * outside itself. That is the whole design, and it is a direct answer to how the
19
+ * old layout failed: there, a directory was named after a record's PARENT, so
20
+ * reading the store meant following links, and one absent record silently cut off
21
+ * every patch written after it.
22
+ *
23
+ * The invariant this module exists to hold up is narrow and worth stating: **the
24
+ * announced set and the delivered set are the same array.** `getStat` and
25
+ * `fetchPatches` both come out of one `readPatchStore` call, so they cannot report
26
+ * different numbers - which is exactly what they did when this broke, announcing
27
+ * 410 patches and delivering 359 with no error in between.
28
+ */
29
+ export declare const PATCH_REPAIR_LOG_FILE_NAME = "patches.repair.log";
30
+ export declare const FSPatch: z.ZodObject<{
31
+ path: z.ZodString & z.ZodType<ModuleFilePath, string, z.core.$ZodTypeInternals<ModuleFilePath, string>>;
32
+ patch: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
33
+ op: z.ZodLiteral<"add">;
34
+ path: z.ZodArray<z.ZodString>;
35
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
36
+ }, z.core.$strict>, z.ZodObject<{
37
+ op: z.ZodLiteral<"remove">;
38
+ path: z.ZodTuple<[z.ZodString], z.ZodString>;
39
+ }, z.core.$strict>, z.ZodObject<{
40
+ op: z.ZodLiteral<"replace">;
41
+ path: z.ZodArray<z.ZodString>;
42
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
43
+ }, z.core.$strict>, z.ZodObject<{
44
+ op: z.ZodLiteral<"move">;
45
+ from: z.ZodTuple<[z.ZodString], z.ZodString>;
46
+ path: z.ZodArray<z.ZodString>;
47
+ }, z.core.$strict>, z.ZodObject<{
48
+ op: z.ZodLiteral<"copy">;
49
+ from: z.ZodArray<z.ZodString>;
50
+ path: z.ZodArray<z.ZodString>;
51
+ }, z.core.$strict>, z.ZodObject<{
52
+ op: z.ZodLiteral<"test">;
53
+ path: z.ZodArray<z.ZodString>;
54
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
55
+ }, z.core.$strict>, z.ZodObject<{
56
+ op: z.ZodLiteral<"file">;
57
+ path: z.ZodArray<z.ZodString>;
58
+ filePath: z.ZodString;
59
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
60
+ remote: z.ZodBoolean;
61
+ nestedFilePath: z.ZodOptional<z.ZodArray<z.ZodString>>;
62
+ metadata: z.ZodOptional<z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>>;
63
+ }, z.core.$strict>], "op">>;
64
+ patchId: z.ZodString;
65
+ baseSha: z.ZodString & z.ZodType<BaseSha, string, z.core.$ZodTypeInternals<BaseSha, string>>;
66
+ authorId: z.ZodNullable<z.ZodString & z.ZodType<AuthorId, string, z.core.$ZodTypeInternals<AuthorId, string>>>;
67
+ createdAt: z.ZodString;
68
+ coreVersion: z.ZodNullable<z.ZodString>;
69
+ sessionId: z.ZodNullable<z.ZodString>;
70
+ }, z.core.$strip>;
71
+ export type FSPatchRecord = z.infer<typeof FSPatch>;
72
+ export declare const FSPatchBase: z.ZodObject<{
73
+ baseSha: z.ZodString & z.ZodType<BaseSha, string, z.core.$ZodTypeInternals<BaseSha, string>>;
74
+ timestamp: z.ZodString;
75
+ }, z.core.$strip>;
76
+ export type FSPatchBaseRecord = z.infer<typeof FSPatchBase>;
77
+ export type PatchStoreEntry = {
78
+ patchId: PatchId;
79
+ record: FSPatchRecord;
80
+ /** Present once the patch has been published. */
81
+ base: FSPatchBaseRecord | null;
82
+ };
83
+ /**
84
+ * Something wrong with the store that a reader can see.
85
+ *
86
+ * Every one of these used to be invisible. Reporting them is the point: the
87
+ * failure that motivated this rewrite was not that the store broke, it was that
88
+ * breaking looked exactly like working.
89
+ */
90
+ export type PatchStoreProblem = {
91
+ type: "log";
92
+ problem: PatchLogProblem;
93
+ }
94
+ /**
95
+ * A directory that cannot be used as a patch: no record, a record that does
96
+ * not parse, or one whose `patchId` is not the directory it sits in.
97
+ *
98
+ * That last case is what a store from before this layout looks like - its
99
+ * directories are named after each record's PARENT - and it is deliberately
100
+ * not special-cased. There is nothing to recover: the order lived in links
101
+ * that are exactly what goes wrong, so an old store is read as a pile of
102
+ * unusable directories and removed like any other.
103
+ *
104
+ * This is the problem the person editing is told about, because it is the one
105
+ * where unpublished work disappears.
106
+ */
107
+ | {
108
+ type: "unreadable-patch";
109
+ /** The directory name, which for a usable patch IS the patch id. */
110
+ name: string;
111
+ dir: string;
112
+ message: string;
113
+ }
114
+ /**
115
+ * A perfectly good record the log does not name.
116
+ *
117
+ * The benign half of a crash: a record is written before its log line, so an
118
+ * interrupted append leaves this behind. Nothing ever read it, so nothing is
119
+ * lost by sweeping it up, and the person editing does not need to hear about
120
+ * a patch that never existed as far as they were concerned.
121
+ */
122
+ | {
123
+ type: "orphan-directory";
124
+ name: string;
125
+ dir: string;
126
+ }
127
+ /**
128
+ * The log was gone, and the order was recovered from the records' timestamps.
129
+ *
130
+ * Reported because it is a guess. Patches written inside the same millisecond
131
+ * have no recoverable order - a real store had eight inside 20ms.
132
+ */
133
+ | {
134
+ type: "reconstructed-log";
135
+ entryCount: number;
136
+ };
137
+ export type ReadPatchStoreResult = {
138
+ status: "ok";
139
+ entries: PatchStoreEntry[];
140
+ problems: PatchStoreProblem[];
141
+ } | {
142
+ status: "unreadable";
143
+ message: string;
144
+ };
145
+ export declare function patchesLogFile(patchesDir: string): string;
146
+ export declare function patchRepairLogFile(patchesDir: string): string;
147
+ export declare function patchDir(patchesDir: string, patchId: PatchId): string;
148
+ export declare function patchRecordFile(patchesDir: string, patchId: PatchId): string;
149
+ export declare function patchBaseFile(patchesDir: string, patchId: PatchId): string;
150
+ export declare function patchBinaryFile(patchesDir: string, patchId: PatchId, filePath: string): string;
151
+ export declare function patchBinaryFileMetadata(patchesDir: string, patchId: PatchId, filePath: string): string;
152
+ /**
153
+ * Read the whole store: the order, the records, and everything wrong with it.
154
+ *
155
+ * One call, one answer. Callers that need only the ids and callers that need the
156
+ * ops both use this, which is what stops them disagreeing.
157
+ */
158
+ export declare function readPatchStore(patchesDir: string): ReadPatchStoreResult;
159
+ /** Write a patch record so that a reader sees all of it or none of it. */
160
+ export declare function writePatchRecord(patchesDir: string, patchId: PatchId, record: FSPatchRecord): void;
161
+ /**
162
+ * Add a patch to the store.
163
+ *
164
+ * Record first, then the log line, and the order is the safety property: an
165
+ * interrupted append leaves a directory nothing points at, which repair sweeps
166
+ * up. The reverse order would leave the log naming a patch that is not there -
167
+ * the exact state this whole rewrite exists to make unreachable.
168
+ *
169
+ * Callers must hold the patch lock.
170
+ */
171
+ export declare function appendPatch(patchesDir: string, record: FSPatchRecord): PatchLogEntry;
172
+ export type RepairAction = {
173
+ type: "removed-unreadable-patch";
174
+ name: string;
175
+ because: string;
176
+ } | {
177
+ type: "removed-orphan-directory";
178
+ name: string;
179
+ } | {
180
+ type: "rewrote-log";
181
+ entryCount: number;
182
+ };
183
+ /**
184
+ * Bring the store back to a state where the log and the directories agree.
185
+ *
186
+ * Safe in a way the old layout's repair could never be: the log is a flat list,
187
+ * so dropping an entry does not orphan the entries after it. There is nothing to
188
+ * re-link, which is why this can run unattended where re-parenting a chain could
189
+ * not.
190
+ *
191
+ * A patch that cannot be read is removed rather than kept around to fail again
192
+ * on every load. That does discard unpublished work, which is why
193
+ * {@link RepairAction} carries the reason, why it is written to
194
+ * `patches.repair.log`, and why the caller is expected to tell the person
195
+ * editing.
196
+ *
197
+ * Callers must hold the patch lock.
198
+ */
199
+ export declare function repairPatchStore(patchesDir: string, read: Extract<ReadPatchStoreResult, {
200
+ status: "ok";
201
+ }>): RepairAction[];
202
+ /**
203
+ * Last resort: move the whole store aside and start empty.
204
+ *
205
+ * A rename, never a delete. What is being given up on here is someone's
206
+ * unpublished work, and the least this can do is say where it went.
207
+ *
208
+ * Callers must hold the patch lock.
209
+ */
210
+ export declare function resetPatchStore(patchesDir: string, reason: string): {
211
+ movedTo: string;
212
+ } | {
213
+ error: string;
214
+ };
215
+ /** One line per problem, for a log line or an API error a person has to act on. */
216
+ export declare function describePatchStoreProblems(problems: readonly PatchStoreProblem[]): string[];