@valbuild/server 0.103.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.
- package/dist/declarations/src/ValOps.d.ts +9 -0
- package/dist/declarations/src/ValOpsFS.d.ts +46 -37
- package/dist/declarations/src/index.d.ts +7 -0
- package/dist/declarations/src/patchLog.d.ts +124 -0
- package/dist/declarations/src/patchStore.d.ts +216 -0
- package/dist/valbuild-server.cjs.dev.js +1345 -341
- package/dist/valbuild-server.cjs.prod.js +1345 -341
- package/dist/valbuild-server.esm.js +1344 -343
- package/package.json +2 -2
|
@@ -84,6 +84,15 @@ export declare abstract class ValOps {
|
|
|
84
84
|
schemaSha: SchemaSha;
|
|
85
85
|
sourcesSha: SourcesSha;
|
|
86
86
|
patches: PatchId[];
|
|
87
|
+
/**
|
|
88
|
+
* Unpublished changes the store threw away because it could not read
|
|
89
|
+
* them. FS mode only: the content api owns its own patches and does not
|
|
90
|
+
* discard them behind the client's back.
|
|
91
|
+
*/
|
|
92
|
+
removed?: {
|
|
93
|
+
patchId: PatchId;
|
|
94
|
+
reason: string;
|
|
95
|
+
}[];
|
|
87
96
|
/** FS mode only — see the `params` counterpart. */
|
|
88
97
|
jsonEntriesSha?: string;
|
|
89
98
|
} | {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { PatchId, ModuleFilePath, ValModules } from "@valbuild/core";
|
|
2
|
-
import { AuthorId, BaseSha, BinaryFileType, GenericErrorMessage, MetadataOfType, OpsMetadata, PreparedCommit, ValOps, ValOpsOptions, WithGenericError, SaveSourceFilePatchResult, SchemaSha, CommitSha, OrderedPatches, OrderedPatchesMetadata,
|
|
2
|
+
import { AuthorId, BaseSha, BinaryFileType, GenericErrorMessage, MetadataOfType, OpsMetadata, PreparedCommit, ValOps, ValOpsOptions, WithGenericError, SaveSourceFilePatchResult, SchemaSha, CommitSha, OrderedPatches, OrderedPatchesMetadata, SourcesSha } from "./ValOps.js";
|
|
3
3
|
import { Patch, ParentRef, ValCommit } from "@valbuild/shared/internal";
|
|
4
|
-
import { ParentPatchId } from "@valbuild/core";
|
|
5
4
|
import { Buffer } from "buffer";
|
|
6
5
|
export declare class ValOpsFS extends ValOps {
|
|
7
6
|
private readonly contentUrl;
|
|
@@ -14,6 +13,20 @@ export declare class ValOpsFS extends ValOps {
|
|
|
14
13
|
* content lives behind a thunk that `JSON.stringify` drops).
|
|
15
14
|
*/
|
|
16
15
|
private readonly jsonEntryFilesFingerprint;
|
|
16
|
+
/**
|
|
17
|
+
* Unpublished changes repair threw away, waiting to be told to somebody.
|
|
18
|
+
*
|
|
19
|
+
* Held here and drained by the next `getStat`, because stat is the channel
|
|
20
|
+
* that always flows. The case worth reporting is a repair that removed
|
|
21
|
+
* EVERYTHING - which is what an old store looks like - and then there is
|
|
22
|
+
* nothing left for the studio to fetch, so a notice riding on `fetchPatches`
|
|
23
|
+
* would never be collected. Drained rather than kept, because this has to be
|
|
24
|
+
* said once: a permanent flag would put a toast on the screen forever.
|
|
25
|
+
*
|
|
26
|
+
* Held in memory only: a restart loses it, and that is the right trade. The
|
|
27
|
+
* durable record is `patches.repair.log`.
|
|
28
|
+
*/
|
|
29
|
+
private readonly removedPatchNotices;
|
|
17
30
|
onInit(): Promise<void>;
|
|
18
31
|
getPresignedAuthNonce(project: string, corsOrigin: string, auth: {
|
|
19
32
|
pat: string;
|
|
@@ -50,6 +63,10 @@ export declare class ValOpsFS extends ValOps {
|
|
|
50
63
|
schemaSha: SchemaSha;
|
|
51
64
|
sourcesSha: SourcesSha;
|
|
52
65
|
patches: PatchId[];
|
|
66
|
+
removed?: {
|
|
67
|
+
patchId: PatchId;
|
|
68
|
+
reason: string;
|
|
69
|
+
}[];
|
|
53
70
|
jsonEntriesSha?: string;
|
|
54
71
|
} | {
|
|
55
72
|
type: "use-websocket";
|
|
@@ -67,23 +84,39 @@ export declare class ValOpsFS extends ValOps {
|
|
|
67
84
|
unauthorized?: boolean;
|
|
68
85
|
networkError?: boolean;
|
|
69
86
|
}>;
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The one read every other read comes out of.
|
|
89
|
+
*
|
|
90
|
+
* `getStat` announces from this and `fetchPatches` delivers from this, so the
|
|
91
|
+
* two cannot disagree. They used to: `getStat` counted the directories on disk
|
|
92
|
+
* while `fetchPatches` walked the parent links between them, and when a single
|
|
93
|
+
* record went missing the first said 410 and the second said 359 — with no
|
|
94
|
+
* error anywhere, because a walk that runs out of links just stops.
|
|
95
|
+
*
|
|
96
|
+
* Anything wrong is reported, then repaired, and reset only if repair does not
|
|
97
|
+
* settle it. In that order, and never silently.
|
|
98
|
+
*/
|
|
99
|
+
private readStore;
|
|
100
|
+
/**
|
|
101
|
+
* Report, repair, and reset only if repair did not take.
|
|
102
|
+
*
|
|
103
|
+
* Reached only when something is actually wrong, so the healthy path — which
|
|
104
|
+
* is every stat poll — never touches the lock.
|
|
105
|
+
*/
|
|
106
|
+
private repairStore;
|
|
72
107
|
fetchPatches<ExcludePatchOps extends boolean>(filters: {
|
|
73
108
|
patchIds?: PatchId[];
|
|
74
109
|
excludePatchOps: ExcludePatchOps;
|
|
75
110
|
}): Promise<ExcludePatchOps extends true ? OrderedPatchesMetadata : OrderedPatches>;
|
|
76
|
-
fetchPatchesFromFS<ExcludePatchOps extends boolean>(excludePatchOps: ExcludePatchOps): Promise<ExcludePatchOps extends true ? FSPatchesMetadata : FSPatches>;
|
|
77
|
-
private createPatchChain;
|
|
78
111
|
private parseJsonFile;
|
|
79
|
-
protected saveSourceFilePatch(path: ModuleFilePath, patch: Patch, patchId: PatchId,
|
|
112
|
+
protected saveSourceFilePatch(path: ModuleFilePath, patch: Patch, patchId: PatchId, _parentRef: ParentRef, authorId: AuthorId | null, sessionId: string | null): Promise<SaveSourceFilePatchResult>;
|
|
80
113
|
protected getSourceFile(path: string): Promise<WithGenericError<{
|
|
81
114
|
data: string;
|
|
82
115
|
}>>;
|
|
83
116
|
protected saveSourceFile(path: ModuleFilePath, data: string): Promise<WithGenericError<{
|
|
84
117
|
path: ModuleFilePath;
|
|
85
118
|
}>>;
|
|
86
|
-
saveBase64EncodedBinaryFileFromPatch(filePath: string,
|
|
119
|
+
saveBase64EncodedBinaryFileFromPatch(filePath: string, _parentRef: ParentRef, patchId: PatchId, data: string | null, _type: BinaryFileType, metadata: MetadataOfType<BinaryFileType> | undefined): Promise<WithGenericError<{
|
|
87
120
|
patchId: PatchId;
|
|
88
121
|
filePath: string;
|
|
89
122
|
}>>;
|
|
@@ -104,7 +137,6 @@ export declare class ValOpsFS extends ValOps {
|
|
|
104
137
|
deleteAllPatches(): Promise<{
|
|
105
138
|
error?: GenericErrorMessage;
|
|
106
139
|
}>;
|
|
107
|
-
private updateOrderedPatches;
|
|
108
140
|
saveOrUploadFiles(preparedCommit: PreparedCommit, mode: "skip-remote" | "upload-remote", auth?: {
|
|
109
141
|
apiKey: string;
|
|
110
142
|
} | {
|
|
@@ -118,33 +150,10 @@ export declare class ValOpsFS extends ValOps {
|
|
|
118
150
|
}>;
|
|
119
151
|
getBinaryFile(filePath: string): Promise<Buffer | null>;
|
|
120
152
|
protected getBinaryFileMetadata<T extends BinaryFileType>(filePath: string, type: T): Promise<OpsMetadata<T>>;
|
|
121
|
-
private getParentPatchIdFromPatchId;
|
|
122
|
-
private getParentPatchIdFromPatchIdMap;
|
|
123
153
|
private getPatchesDir;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
private
|
|
154
|
+
/**
|
|
155
|
+
* Deliberately outside the patches directory: delete-all and reset rename that
|
|
156
|
+
* whole directory, and a lock that moves away with it is not holding anything.
|
|
157
|
+
*/
|
|
158
|
+
private getPatchLockFile;
|
|
129
159
|
}
|
|
130
|
-
type FSPatches = {
|
|
131
|
-
patches: Record<PatchId, {
|
|
132
|
-
path: ModuleFilePath;
|
|
133
|
-
patch: Patch;
|
|
134
|
-
parentRef: ParentRef;
|
|
135
|
-
createdAt: string;
|
|
136
|
-
authorId: AuthorId | null;
|
|
137
|
-
baseSha: BaseSha;
|
|
138
|
-
appliedAt: null;
|
|
139
|
-
}>;
|
|
140
|
-
error?: GenericErrorMessage;
|
|
141
|
-
errors?: PatchReadError[];
|
|
142
|
-
};
|
|
143
|
-
type FSPatchesMetadata = {
|
|
144
|
-
patches: Record<PatchId, Omit<FSPatches["patches"][PatchId], "patch"> & {
|
|
145
|
-
patch?: undefined;
|
|
146
|
-
}>;
|
|
147
|
-
error?: GenericErrorMessage;
|
|
148
|
-
errors?: FSPatches["errors"];
|
|
149
|
-
};
|
|
150
|
-
export {};
|
|
@@ -38,3 +38,10 @@ export { formatPatchSourceError } from "./ValOps.js";
|
|
|
38
38
|
export { compareWithCapturedReport, readCapturedReport, replaySnapshot, } from "./debug/replaySnapshot.js";
|
|
39
39
|
export type { ReplayComparison, ReplayResult } from "./debug/replaySnapshot.js";
|
|
40
40
|
export type { OrderedPatches, PatchAnalysis, PatchSourceError, PreparedCommit, } from "./ValOps.js";
|
|
41
|
+
/**
|
|
42
|
+
* The local-dev patch store, exported so the CLI's debug tooling can read a
|
|
43
|
+
* snapshot back with the same code the server uses rather than a second
|
|
44
|
+
* implementation of the layout.
|
|
45
|
+
*/
|
|
46
|
+
export { readPatchStore, describePatchStoreProblems } from "./patchStore.js";
|
|
47
|
+
export type { PatchStoreEntry, PatchStoreProblem, ReadPatchStoreResult, } from "./patchStore.js";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ModuleFilePath, PatchId } from "@valbuild/core";
|
|
2
|
+
/**
|
|
3
|
+
* The order of the pending patches, held in one flat, append-only text file.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a log and not links between the patches
|
|
6
|
+
*
|
|
7
|
+
* The store this replaces kept the order in the patches themselves: every record
|
|
8
|
+
* carried a `parentRef`, and the directory a record lived in was named after that
|
|
9
|
+
* parent. Reading the chain meant walking those links from `head`, and the walk
|
|
10
|
+
* stopped dead — silently — at the first id nothing on disk answered for.
|
|
11
|
+
*
|
|
12
|
+
* That is not a hypothetical. A store with 410 patches lost exactly one record,
|
|
13
|
+
* and the 51 patches written after it became unreachable: `/stat` counted the
|
|
14
|
+
* directories and announced 410, `GET /patches` walked the links and delivered
|
|
15
|
+
* 359, and the studio waited forever for 51 ids that no longer had a path back to
|
|
16
|
+
* `head`. One missing file cost every edit made after it.
|
|
17
|
+
*
|
|
18
|
+
* So the order lives in one place and the patches reference nothing. A patch
|
|
19
|
+
* directory is self-contained data; this file says what order the directories go
|
|
20
|
+
* in. Removing a line cannot orphan the lines below it, because there is nothing
|
|
21
|
+
* below it to orphan — the entry after a dropped one is simply next.
|
|
22
|
+
*
|
|
23
|
+
* ## Why text
|
|
24
|
+
*
|
|
25
|
+
* It is read by people during exactly the incidents that make it interesting, and
|
|
26
|
+
* `cat` should be enough. One entry per line, whitespace-separated, id first:
|
|
27
|
+
*
|
|
28
|
+
* ```
|
|
29
|
+
* val-patch-log v1
|
|
30
|
+
* 659b8cfa-065c-47d1-8d82-fc69a2ac72a9 2026-08-27T11:46:13.730Z /content/authors.val.ts
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Position in the file IS the order. There is deliberately no sequence number:
|
|
34
|
+
* a number stored beside the thing it describes is a number that can disagree
|
|
35
|
+
* with it, and then something has to decide which one lies.
|
|
36
|
+
*
|
|
37
|
+
* The timestamp and path are there to make the file readable, not to be believed
|
|
38
|
+
* — `patch.json` owns those fields. Nothing here is a second copy of state that
|
|
39
|
+
* anything reads back.
|
|
40
|
+
*/
|
|
41
|
+
export declare const PATCH_LOG_FILE_NAME = "patches.log";
|
|
42
|
+
export type PatchLogEntry = {
|
|
43
|
+
patchId: PatchId;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
path: ModuleFilePath;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Something that is wrong with the file but does not stop it being read.
|
|
49
|
+
*
|
|
50
|
+
* Reported rather than thrown, because a log that is 99% intact is worth reading
|
|
51
|
+
* and then repairing. Only a file that cannot be understood at all is fatal, and
|
|
52
|
+
* that is a `status` on the read result, not a problem in this list.
|
|
53
|
+
*/
|
|
54
|
+
export type PatchLogProblem = {
|
|
55
|
+
type: "missing-header";
|
|
56
|
+
/** What stood where the header should have been. */
|
|
57
|
+
firstLine: string;
|
|
58
|
+
} | {
|
|
59
|
+
type: "unsupported-version";
|
|
60
|
+
header: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: "unparseable-line";
|
|
63
|
+
lineNumber: number;
|
|
64
|
+
line: string;
|
|
65
|
+
} | {
|
|
66
|
+
type: "duplicate-entry";
|
|
67
|
+
patchId: PatchId;
|
|
68
|
+
lineNumber: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A final line with no newline after it: a write that did not finish.
|
|
72
|
+
*
|
|
73
|
+
* Discarded rather than guessed at, and only ever possible on the LAST line —
|
|
74
|
+
* appends are serialized by the patch lock, so no other writer can have got in
|
|
75
|
+
* behind an interrupted one.
|
|
76
|
+
*/
|
|
77
|
+
| {
|
|
78
|
+
type: "torn-final-line";
|
|
79
|
+
line: string;
|
|
80
|
+
};
|
|
81
|
+
export type ReadPatchLogResult = {
|
|
82
|
+
status: "ok";
|
|
83
|
+
entries: PatchLogEntry[];
|
|
84
|
+
problems: PatchLogProblem[];
|
|
85
|
+
}
|
|
86
|
+
/** No log file at all — an empty store, not a broken one. */
|
|
87
|
+
| {
|
|
88
|
+
status: "absent";
|
|
89
|
+
} | {
|
|
90
|
+
status: "unreadable";
|
|
91
|
+
message: string;
|
|
92
|
+
};
|
|
93
|
+
export declare function formatPatchLogLine(entry: PatchLogEntry): string;
|
|
94
|
+
/**
|
|
95
|
+
* Split on the first two runs of whitespace only: a module file path may contain
|
|
96
|
+
* spaces, and it is last precisely so that it can.
|
|
97
|
+
*/
|
|
98
|
+
export declare function parsePatchLogLine(line: string): PatchLogEntry | null;
|
|
99
|
+
export declare function serializePatchLog(entries: readonly PatchLogEntry[]): string;
|
|
100
|
+
export declare function readPatchLog(logFilePath: string): ReadPatchLogResult;
|
|
101
|
+
export declare function parsePatchLog(raw: string): ReadPatchLogResult;
|
|
102
|
+
/**
|
|
103
|
+
* Add one entry to the end of the log.
|
|
104
|
+
*
|
|
105
|
+
* A single `writeSync` on an append-only descriptor, then fsync: the whole line
|
|
106
|
+
* reaches the file or none of it does, and a reader that catches it mid-flight
|
|
107
|
+
* discards the partial last line rather than misreading it.
|
|
108
|
+
*
|
|
109
|
+
* Callers must hold the patch lock. That is what makes "the torn line can only be
|
|
110
|
+
* the last one" true, and it is why this does not try to be safe against
|
|
111
|
+
* concurrent appends on its own.
|
|
112
|
+
*/
|
|
113
|
+
export declare function appendPatchLogEntry(logFilePath: string, entry: PatchLogEntry): void;
|
|
114
|
+
/**
|
|
115
|
+
* Replace the log wholesale — used by delete and by repair.
|
|
116
|
+
*
|
|
117
|
+
* Write a sibling temp file, fsync it, then rename over the original: a rename is
|
|
118
|
+
* atomic, so a reader either sees the old log or the new one and never a
|
|
119
|
+
* half-rewritten file. In-place truncation would have a window where the log is
|
|
120
|
+
* short and the store looks like it lost patches.
|
|
121
|
+
*
|
|
122
|
+
* Callers must hold the patch lock.
|
|
123
|
+
*/
|
|
124
|
+
export declare function writePatchLogFile(logFilePath: string, entries: readonly PatchLogEntry[]): void;
|
|
@@ -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[];
|