@valbuild/server 0.103.2 → 0.105.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.
@@ -3,7 +3,7 @@ import { result } from "@valbuild/core/fp";
3
3
  import { JSONValue, ParentRef, Patch, PatchError } from "@valbuild/core/patch";
4
4
  import { ValSyntaxError, ValSyntaxErrorTree } from "./patch/ts/syntax.js";
5
5
  import { ParentPatchId } from "@valbuild/core";
6
- import type { ReifiedRender } from "@valbuild/core";
6
+ import type { ReifiedPreview } from "@valbuild/core";
7
7
  import { ValCommit, ValDeployment } from "@valbuild/shared/internal";
8
8
  export type BaseSha = string & {
9
9
  readonly _tag: unique symbol;
@@ -57,6 +57,40 @@ export declare abstract class ValOps {
57
57
  /** The sha256 / hash of schema + config - if this changes users needs to reload */
58
58
  private schemaSha;
59
59
  private modulesErrors;
60
+ /**
61
+ * What the SHAs above are a fold over, so they can be recomputed.
62
+ *
63
+ * See {@link promoteCommittedSources}: the one thing that changes sources
64
+ * without re-evaluating the modules is a save, and it has to be able to move
65
+ * the SHAs with them.
66
+ */
67
+ private shaEntries;
68
+ /**
69
+ * The extraction's OWN module errors, which are what the fold was given.
70
+ *
71
+ * Not the same list as {@link modulesErrors}: that one has the nested
72
+ * `.jsonValues()` errors concatenated on, and those were never part of the
73
+ * hash. Re-folding with the wrong list changes the base SHA for no reason.
74
+ */
75
+ private shaModuleErrors;
76
+ /**
77
+ * What a save has told us each `.jsonValues()` entry now holds.
78
+ *
79
+ * The entry twin of {@link sources}, and it has to be separate because an
80
+ * entry's content is not IN the source: the source holds a marker, and
81
+ * {@link getJsonEntries} resolves it by awaiting the marker's own `import()`.
82
+ * That resolves from the module registry, so after `/save` rewrites a
83
+ * `*.val.json` the thunk keeps answering with the content from before — and
84
+ * unlike a module source there is nothing to re-extract, because the memo was
85
+ * never holding the content in the first place.
86
+ *
87
+ * `null` for an entry the commit deleted.
88
+ *
89
+ * Never cleared: it describes what is on disk. A host rebuild makes a new
90
+ * instance, which is the right reset. Bounded by the project's entry count,
91
+ * holding only the latest content per key.
92
+ */
93
+ private adoptedJsonEntries;
60
94
  constructor(valModules: ValModules, options?: ValOpsOptions | undefined);
61
95
  /**
62
96
  * Get the status from Val
@@ -111,6 +145,67 @@ export declare abstract class ValOps {
111
145
  networkError?: boolean;
112
146
  }>;
113
147
  private initSources;
148
+ /**
149
+ * These patches are on disk now: adopt what they produced as the committed
150
+ * sources.
151
+ *
152
+ * The entry point for the mechanism {@link promoteCommittedSources} describes,
153
+ * and the only one — a caller hands over the analysis it just committed and
154
+ * this works out the rest, so the rule about which sources are adopted lives
155
+ * in one place rather than at each save site.
156
+ *
157
+ * A module whose patches could not be applied cleanly is left alone. `/save`
158
+ * refuses the whole commit before reaching here if `prepare` found errors, so
159
+ * this cannot normally fire — but adopting a partially patched source would
160
+ * put content in the memo that is not what was written, which is worse than
161
+ * being stale.
162
+ */
163
+ adoptCommittedSources(analysis: PatchAnalysis & OrderedPatches, preparedCommit: Pick<PreparedCommit, "patchedJsonEntries">): Promise<void>;
164
+ /**
165
+ * Adopt sources that have just been written to disk, and move the SHAs with
166
+ * them.
167
+ *
168
+ * ## Why this exists rather than an invalidation
169
+ *
170
+ * The obvious thing — throw the memo away after a save so the next read
171
+ * re-extracts — does not work, and quietly. `extractValModules` gets a
172
+ * module's content by awaiting its `def`, which is the app's own `import()`:
173
+ * that resolves from the MODULE REGISTRY, not from the file on disk. Right
174
+ * after `/save` rewrites a `.val.ts`, the registry still holds the module as
175
+ * it was evaluated before, so a re-extraction returns the pre-save content and
176
+ * stores it as fresh. What actually replaces it is the host rebuilding its
177
+ * module graph and constructing a new `ValOps` — which happens on its own
178
+ * schedule, and until it does, every read is stale.
179
+ *
180
+ * Stale reads here are not abstract: `getJsonEntry` resolves a
181
+ * `.jsonValues()` entry from the committed source and then replays pending
182
+ * patches over it, so once a publish has removed the patches, a page rendering
183
+ * draft content gets the committed value — the one this memo is holding from
184
+ * before the publish.
185
+ *
186
+ * So the save tells us instead. It has just computed what the new committed
187
+ * sources are, and that answer does not depend on anything being
188
+ * re-evaluated.
189
+ *
190
+ * ## And the SHAs move
191
+ *
192
+ * Deliberately, and this is the part with consequences. `baseSha` and
193
+ * `sourcesSha` identify the sources being served; leaving them still while the
194
+ * sources move would put a value other code compares against into
195
+ * disagreement with what it describes. Moving them means a `fs`-mode base SHA
196
+ * changes within a server's lifetime for the first time, which is a signal the
197
+ * studio already knows how to read: `PatchStore.reconcileVanished` uses a
198
+ * moved base to tell "these patches were published" from "these patches were
199
+ * discarded", and takes them out of the chain without reverting the fields —
200
+ * which is what a second tab watching a publish needs and could not get
201
+ * before.
202
+ *
203
+ * A module the fold does not know is ignored rather than appended: the fold's
204
+ * order is `val.modules`, and a path that is not in it has no position, so
205
+ * there is no honest answer for where its hash would go. It also cannot happen
206
+ * — a save only ever writes modules it read from here.
207
+ */
208
+ protected promoteCommittedSources(patched: Sources): void;
114
209
  init(): Promise<void>;
115
210
  getBaseSources(): Promise<Sources>;
116
211
  /**
@@ -198,16 +293,19 @@ export declare abstract class ValOps {
198
293
  getSchemaSha(): Promise<SchemaSha>;
199
294
  analyzePatches(sortedPatches: OrderedPatches["patches"], commits?: ValCommit[], currentCommitSha?: CommitSha): PatchAnalysis;
200
295
  /**
201
- * Reifies each module's render from its schema INSTANCE.
296
+ * Reifies each module's previews from its schema INSTANCE.
202
297
  *
203
- * Kept even though the Studio also computes renders client-side: `select` is a
204
- * user function that lives on the instance and is not part of the serialized
298
+ * Kept even though the Studio also computes previews client-side: a preview is
299
+ * a user function that lives on the instance and is not part of the serialized
205
300
  * schema, so a host app that does not render `<ValModulesClient>` has no
206
- * instances in the browser and would otherwise get no renders at all. See
301
+ * instances in the browser and would otherwise get no previews at all. See
207
302
  * #470.
303
+ *
304
+ * A string's `render` needs none of this — it is static config that travels
305
+ * with the serialized schema.
208
306
  */
209
- getRenders(schemas: Schemas, sources: Sources): Promise<{
210
- renders: Record<ModuleFilePath, ReifiedRender | null>;
307
+ getPreviews(schemas: Schemas, sources: Sources): Promise<{
308
+ previews: Record<ModuleFilePath, ReifiedPreview | null>;
211
309
  }>;
212
310
  getSources(analysis?: PatchAnalysis & OrderedPatches): Promise<{
213
311
  sources: Sources;
@@ -365,6 +463,23 @@ export type PreparedCommit = {
365
463
  * A null value signals that the file at that path should be deleted.
366
464
  */
367
465
  patchedSourceFiles: Record<string, string | null>;
466
+ /**
467
+ * The committed content of every `.jsonValues()` entry this commit changed,
468
+ * per module and entry key. `null` means the entry was deleted.
469
+ *
470
+ * Separate from {@link patchedSourceFiles} rather than folded into it, because
471
+ * that map is keyed by FILE PATH and a reader of an entry has a module and a
472
+ * key. A marker does not carry its path at read time, so the two are not
473
+ * interchangeable — see `jsonEntryFiles.ts`.
474
+ *
475
+ * Here for {@link ValOps.adoptCommittedSources}: an entry's committed content
476
+ * is resolved through the marker's own `import()`, which caches, so a save is
477
+ * the only thing that can tell the server what the entry now holds.
478
+ *
479
+ * Only modules whose patches applied cleanly appear; a module that errored
480
+ * contributes nothing, and `/save` refuses the commit anyway.
481
+ */
482
+ patchedJsonEntries: Record<ModuleFilePath, Record<string, JSONValue | null>>;
368
483
  /**
369
484
  * Previous source files that were patched
370
485
  */
@@ -120,6 +120,15 @@ export declare class ValOpsFS extends ValOps {
120
120
  patchId: PatchId;
121
121
  filePath: string;
122
122
  }>>;
123
+ /**
124
+ * Which of the two places a patch's file can be, if either.
125
+ *
126
+ * The bytes are in the store once the patch's record is, and in the staging
127
+ * area before that — see `uploadsDir`. Every reader has to accept both, and
128
+ * they decide it here rather than each on its own, so two readers of the same
129
+ * file cannot disagree about whether it exists.
130
+ */
131
+ private wherePatchFileIs;
123
132
  protected getBase64EncodedBinaryFileMetadataFromPatch<T extends BinaryFileType>(filePath: string, type: T, patchId: PatchId): Promise<OpsMetadata<T>>;
124
133
  getBase64EncodedBinaryFileFromPatch(filePath: string, patchId: PatchId): Promise<Buffer | null>;
125
134
  deletePatches(patchIds: PatchId[]): Promise<{
@@ -5,4 +5,4 @@ import { ValidationError } from "@valbuild/core";
5
5
  * There is no schema in hand here — a `ValidationError` carries only the value
6
6
  * it flagged — so the shape is all there is to go on.
7
7
  */
8
- export declare function getValidationErrorFileRef(validationError: ValidationError): string | null;
8
+ export declare function getValidationErrorFileRef(validationError: ValidationError): any;
@@ -1,4 +1,4 @@
1
- import { ModuleFilePath, PatchId } from "@valbuild/core";
1
+ import { PatchId } from "@valbuild/core";
2
2
  import { z } from "zod";
3
3
  import type { AuthorId, BaseSha } from "./ValOps.js";
4
4
  import { PatchLogEntry, PatchLogProblem } from "./patchLog.js";
@@ -32,14 +32,14 @@ export declare const FSPatch: z.ZodObject<{
32
32
  patch: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
33
33
  op: z.ZodLiteral<"add">;
34
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>>;
35
+ value: z.ZodType<JSONValueT, unknown, z.core.$ZodTypeInternals<JSONValueT, unknown>>;
36
36
  }, z.core.$strict>, z.ZodObject<{
37
37
  op: z.ZodLiteral<"remove">;
38
38
  path: z.ZodTuple<[z.ZodString], z.ZodString>;
39
39
  }, z.core.$strict>, z.ZodObject<{
40
40
  op: z.ZodLiteral<"replace">;
41
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>>;
42
+ value: z.ZodType<JSONValueT, unknown, z.core.$ZodTypeInternals<JSONValueT, unknown>>;
43
43
  }, z.core.$strict>, z.ZodObject<{
44
44
  op: z.ZodLiteral<"move">;
45
45
  from: z.ZodTuple<[z.ZodString], z.ZodString>;
@@ -51,15 +51,15 @@ export declare const FSPatch: z.ZodObject<{
51
51
  }, z.core.$strict>, z.ZodObject<{
52
52
  op: z.ZodLiteral<"test">;
53
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>>;
54
+ value: z.ZodType<JSONValueT, unknown, z.core.$ZodTypeInternals<JSONValueT, unknown>>;
55
55
  }, z.core.$strict>, z.ZodObject<{
56
56
  op: z.ZodLiteral<"file">;
57
57
  path: z.ZodArray<z.ZodString>;
58
58
  filePath: z.ZodString;
59
- value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
59
+ value: z.ZodType<JSONValueT, unknown, z.core.$ZodTypeInternals<JSONValueT, unknown>>;
60
60
  remote: z.ZodBoolean;
61
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>>>;
62
+ metadata: z.ZodOptional<z.ZodType<JSONValueT, unknown, z.core.$ZodTypeInternals<JSONValueT, unknown>>>;
63
63
  }, z.core.$strict>], "op">>;
64
64
  patchId: z.ZodString;
65
65
  baseSha: z.ZodString & z.ZodType<BaseSha, string, z.core.$ZodTypeInternals<BaseSha, string>>;
@@ -147,8 +147,53 @@ export declare function patchRepairLogFile(patchesDir: string): string;
147
147
  export declare function patchDir(patchesDir: string, patchId: PatchId): string;
148
148
  export declare function patchRecordFile(patchesDir: string, patchId: PatchId): string;
149
149
  export declare function patchBaseFile(patchesDir: string, patchId: PatchId): string;
150
+ /**
151
+ * Where a patch's uploaded bytes wait for the record that will reference them.
152
+ *
153
+ * ## Why they cannot simply be written into the patch directory
154
+ *
155
+ * A patch that carries a file is written in TWO requests, and the bytes go
156
+ * first: the record's `file` op holds only a sha, so a record written before its
157
+ * bytes would point at nothing. Uploading straight into `<patchId>/files/` left
158
+ * the directory holding files and no `patch.json` for the length of a round
159
+ * trip — which is neither of the two shapes this store allows, so
160
+ * {@link readPatchStore} read it as a patch whose contents were lost and repair
161
+ * removed it, bytes and all.
162
+ *
163
+ * And that window is not passive: writing into the patches directory is exactly
164
+ * what ends `getStat`'s long poll, so the upload summoned the read that
165
+ * destroyed it. Replacing an image worked only when the two requests happened to
166
+ * land close enough together.
167
+ *
168
+ * So the bytes are not in the store until they belong to something.
169
+ * {@link appendPatch} moves them in after writing the record, under the lock, so
170
+ * no reader ever sees a half-built patch directory — and the invariant that a
171
+ * directory either holds a usable record or is named by the log holds again,
172
+ * with nothing to tolerate and no ambiguous state to classify.
173
+ *
174
+ * A SIBLING of the patches directory, for two reasons: nothing that reads the
175
+ * store lists it, and it is on the same filesystem, so moving into place is a
176
+ * rename rather than a copy.
177
+ */
178
+ export declare function uploadsDir(patchesDir: string): string;
179
+ /** Where one patch's uploads wait. See {@link uploadsDir}. */
180
+ export declare function patchUploadDir(patchesDir: string, patchId: PatchId): string;
150
181
  export declare function patchBinaryFile(patchesDir: string, patchId: PatchId, filePath: string): string;
151
182
  export declare function patchBinaryFileMetadata(patchesDir: string, patchId: PatchId, filePath: string): string;
183
+ /** The staged twin of {@link patchBinaryFile}. */
184
+ export declare function stagedPatchBinaryFile(patchesDir: string, patchId: PatchId, filePath: string): string;
185
+ /** The staged twin of {@link patchBinaryFileMetadata}. */
186
+ export declare function stagedPatchBinaryFileMetadata(patchesDir: string, patchId: PatchId, filePath: string): string;
187
+ /** Drop a patch's staging directory, whatever is left of it. */
188
+ export declare function removeStagedUploads(patchesDir: string, patchId: PatchId): void;
189
+ /**
190
+ * Drop staged uploads whose patch never arrived.
191
+ *
192
+ * A client that dies between the upload and the `PUT` leaves its bytes here.
193
+ * Nothing references them — no record points at them and the log never named
194
+ * them — so they are removed without a word.
195
+ */
196
+ export declare function sweepStaleUploads(patchesDir: string, now?: number): void;
152
197
  /**
153
198
  * Read the whole store: the order, the records, and everything wrong with it.
154
199
  *