@valbuild/server 0.103.1 → 0.105.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/dist/declarations/src/ValOps.d.ts +10 -7
- package/dist/declarations/src/getValidationErrorFileRef.d.ts +1 -1
- package/dist/declarations/src/patchStore.d.ts +6 -6
- package/dist/valbuild-server.cjs.dev.js +201 -20
- package/dist/valbuild-server.cjs.prod.js +201 -20
- package/dist/valbuild-server.esm.js +201 -20
- package/package.json +4 -4
|
@@ -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 {
|
|
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;
|
|
@@ -198,16 +198,19 @@ export declare abstract class ValOps {
|
|
|
198
198
|
getSchemaSha(): Promise<SchemaSha>;
|
|
199
199
|
analyzePatches(sortedPatches: OrderedPatches["patches"], commits?: ValCommit[], currentCommitSha?: CommitSha): PatchAnalysis;
|
|
200
200
|
/**
|
|
201
|
-
* Reifies each module's
|
|
201
|
+
* Reifies each module's previews from its schema INSTANCE.
|
|
202
202
|
*
|
|
203
|
-
* Kept even though the Studio also computes
|
|
204
|
-
* user function that lives on the instance and is not part of the serialized
|
|
203
|
+
* Kept even though the Studio also computes previews client-side: a preview is
|
|
204
|
+
* a user function that lives on the instance and is not part of the serialized
|
|
205
205
|
* schema, so a host app that does not render `<ValModulesClient>` has no
|
|
206
|
-
* instances in the browser and would otherwise get no
|
|
206
|
+
* instances in the browser and would otherwise get no previews at all. See
|
|
207
207
|
* #470.
|
|
208
|
+
*
|
|
209
|
+
* A string's `render` needs none of this — it is static config that travels
|
|
210
|
+
* with the serialized schema.
|
|
208
211
|
*/
|
|
209
|
-
|
|
210
|
-
|
|
212
|
+
getPreviews(schemas: Schemas, sources: Sources): Promise<{
|
|
213
|
+
previews: Record<ModuleFilePath, ReifiedPreview | null>;
|
|
211
214
|
}>;
|
|
212
215
|
getSources(analysis?: PatchAnalysis & OrderedPatches): Promise<{
|
|
213
216
|
sources: Sources;
|
|
@@ -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):
|
|
8
|
+
export declare function getValidationErrorFileRef(validationError: ValidationError): any;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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>>;
|
|
@@ -2045,24 +2045,27 @@ class ValOps {
|
|
|
2045
2045
|
};
|
|
2046
2046
|
}
|
|
2047
2047
|
|
|
2048
|
-
// #region
|
|
2048
|
+
// #region getPreviews
|
|
2049
2049
|
/**
|
|
2050
|
-
* Reifies each module's
|
|
2050
|
+
* Reifies each module's previews from its schema INSTANCE.
|
|
2051
2051
|
*
|
|
2052
|
-
* Kept even though the Studio also computes
|
|
2053
|
-
* user function that lives on the instance and is not part of the serialized
|
|
2052
|
+
* Kept even though the Studio also computes previews client-side: a preview is
|
|
2053
|
+
* a user function that lives on the instance and is not part of the serialized
|
|
2054
2054
|
* schema, so a host app that does not render `<ValModulesClient>` has no
|
|
2055
|
-
* instances in the browser and would otherwise get no
|
|
2055
|
+
* instances in the browser and would otherwise get no previews at all. See
|
|
2056
2056
|
* #470.
|
|
2057
|
+
*
|
|
2058
|
+
* A string's `render` needs none of this — it is static config that travels
|
|
2059
|
+
* with the serialized schema.
|
|
2057
2060
|
*/
|
|
2058
|
-
async
|
|
2059
|
-
const
|
|
2061
|
+
async getPreviews(schemas, sources) {
|
|
2062
|
+
const previews = {};
|
|
2060
2063
|
for (const [pathS, schema] of Object.entries(schemas)) {
|
|
2061
2064
|
const path = pathS;
|
|
2062
|
-
|
|
2065
|
+
previews[path] = schema["executePreview"](path, sources[path]);
|
|
2063
2066
|
}
|
|
2064
2067
|
return {
|
|
2065
|
-
|
|
2068
|
+
previews
|
|
2066
2069
|
};
|
|
2067
2070
|
}
|
|
2068
2071
|
|
|
@@ -5442,6 +5445,112 @@ class FSOpsHost {
|
|
|
5442
5445
|
}
|
|
5443
5446
|
}
|
|
5444
5447
|
|
|
5448
|
+
/**
|
|
5449
|
+
* Which unpublished changes a failed save has to throw away to make progress.
|
|
5450
|
+
*
|
|
5451
|
+
* `/save` used to refuse the whole commit when any one patch could not be
|
|
5452
|
+
* applied: nothing was written, for any module, and the change stayed on disk to
|
|
5453
|
+
* fail again on the next attempt. With auto-save that is not a refusal, it is a
|
|
5454
|
+
* dead stop — the editor keeps typing, every save fails on the same patch, and
|
|
5455
|
+
* nothing is ever written again.
|
|
5456
|
+
*
|
|
5457
|
+
* So in `fs` mode the failing change is removed instead, and the person editing
|
|
5458
|
+
* is told. The rule for what goes with it:
|
|
5459
|
+
*
|
|
5460
|
+
* **From the first failure onward, within that module.** A patch is a set of ops
|
|
5461
|
+
* against the source as the patches before it left it. Once one has not been
|
|
5462
|
+
* applied, every later patch in that module was computed against a state that no
|
|
5463
|
+
* longer exists, so keeping them would be keeping changes whose meaning has
|
|
5464
|
+
* already been lost. That is `triedPatches` (the one that failed) plus
|
|
5465
|
+
* `skippedPatches` (the tail that was never attempted).
|
|
5466
|
+
*
|
|
5467
|
+
* **Other modules are untouched.** `prepare` walks each module's chain
|
|
5468
|
+
* independently, so a broken chain in one file says nothing about another.
|
|
5469
|
+
*
|
|
5470
|
+
* **And a module that failed as a WHOLE keeps everything.** When the `.val.ts`
|
|
5471
|
+
* cannot be read at all, `prepare` reports every one of its patches as skipped
|
|
5472
|
+
* and names none of them as unappliable — the file is what failed, not the
|
|
5473
|
+
* changes. Deleting them there would be destroying good edits because a file was
|
|
5474
|
+
* missing for a moment: rename a module, or switch branch, with unsaved changes
|
|
5475
|
+
* to it and the next auto-save would silently take them all. Skipped is only
|
|
5476
|
+
* collateral when there is something in that module for it to be collateral OF.
|
|
5477
|
+
*/
|
|
5478
|
+
|
|
5479
|
+
/**
|
|
5480
|
+
* The message for a patch that did not itself fail but sits behind one that did.
|
|
5481
|
+
*
|
|
5482
|
+
* Worth distinguishing: "yours was broken" and "something before yours was
|
|
5483
|
+
* broken" are different things to be told, and only the first is a bug in the
|
|
5484
|
+
* change itself.
|
|
5485
|
+
*/
|
|
5486
|
+
function collateralMessage(moduleFilePath) {
|
|
5487
|
+
return `An earlier change to ${moduleFilePath} could not be applied, so this one no longer fits the content it was written against.`;
|
|
5488
|
+
}
|
|
5489
|
+
function computePatchesToDrop(prepared) {
|
|
5490
|
+
const dropped = [];
|
|
5491
|
+
const seen = new Set();
|
|
5492
|
+
const add = (moduleFilePath, patchId) => {
|
|
5493
|
+
var _prepared$unappliable;
|
|
5494
|
+
if (seen.has(patchId)) {
|
|
5495
|
+
return;
|
|
5496
|
+
}
|
|
5497
|
+
seen.add(patchId);
|
|
5498
|
+
dropped.push({
|
|
5499
|
+
patchId,
|
|
5500
|
+
moduleFilePath,
|
|
5501
|
+
message: ((_prepared$unappliable = prepared.unappliablePatches[patchId]) === null || _prepared$unappliable === void 0 ? void 0 : _prepared$unappliable.message) ?? collateralMessage(moduleFilePath)
|
|
5502
|
+
});
|
|
5503
|
+
};
|
|
5504
|
+
|
|
5505
|
+
// Tried before skipped, so the change that actually failed is named first —
|
|
5506
|
+
// it is the one worth reading.
|
|
5507
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5508
|
+
for (const patchId of patchIds) {
|
|
5509
|
+
add(moduleFilePath, patchId);
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
/*
|
|
5513
|
+
* Which modules actually had a change fail, as opposed to being unreadable.
|
|
5514
|
+
*
|
|
5515
|
+
* `triedPatches` holds only failures, and `unappliablePatches` names the
|
|
5516
|
+
* failure itself, so between them they are the whole answer. A module in
|
|
5517
|
+
* neither has nothing to blame its skipped tail on.
|
|
5518
|
+
*/
|
|
5519
|
+
const failedModules = new Set();
|
|
5520
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5521
|
+
if (patchIds.length > 0) {
|
|
5522
|
+
failedModules.add(moduleFilePath);
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
for (const failure of Object.values(prepared.unappliablePatches)) {
|
|
5526
|
+
failedModules.add(failure.moduleFilePath);
|
|
5527
|
+
}
|
|
5528
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.skippedPatches)) {
|
|
5529
|
+
if (!failedModules.has(moduleFilePath)) {
|
|
5530
|
+
// The module failed as a whole. Its changes are fine and are kept, so the
|
|
5531
|
+
// save refuses instead — which is what it did before auto-save, and is
|
|
5532
|
+
// right here: this one resolves itself when the file comes back.
|
|
5533
|
+
continue;
|
|
5534
|
+
}
|
|
5535
|
+
for (const patchId of patchIds) {
|
|
5536
|
+
add(moduleFilePath, patchId);
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
|
|
5540
|
+
/*
|
|
5541
|
+
* A patch reported unappliable that neither list holds.
|
|
5542
|
+
*
|
|
5543
|
+
* Belt and braces: the two lists are how `prepare` reports the shape of the
|
|
5544
|
+
* failure, and this is how it reports the failure itself. If they ever
|
|
5545
|
+
* disagree, leaving a patch behind means the next save fails on it again —
|
|
5546
|
+
* the exact dead stop this function exists to prevent.
|
|
5547
|
+
*/
|
|
5548
|
+
for (const [patchId, failure] of Object.entries(prepared.unappliablePatches)) {
|
|
5549
|
+
add(failure.moduleFilePath, patchId);
|
|
5550
|
+
}
|
|
5551
|
+
return dropped;
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5445
5554
|
const textEncoder$2 = new TextEncoder();
|
|
5446
5555
|
const PatchId = z.z.string().refine(s => !!s); // TODO: validate
|
|
5447
5556
|
const CommitSha = z.z.string().refine(s => !!s); // TODO: validate
|
|
@@ -7870,11 +7979,11 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
7870
7979
|
// The studio client always passes false: it owns patch application
|
|
7871
7980
|
// and validation, and treats /sources/~ as a pure un-patched read.
|
|
7872
7981
|
const applyPatches = query.apply_patches !== false;
|
|
7873
|
-
// NOTE:
|
|
7874
|
-
// itself: the
|
|
7875
|
-
//
|
|
7876
|
-
//
|
|
7877
|
-
//
|
|
7982
|
+
// NOTE: previews are computed here even when the client applies patches
|
|
7983
|
+
// itself: the preview functions live on the Schema instances and are not
|
|
7984
|
+
// part of the serialized schema, so the client cannot derive them. They
|
|
7985
|
+
// are computed on the patched sources, so that previews (list titles /
|
|
7986
|
+
// subtitles / images) reflect the patches that apply.
|
|
7878
7987
|
let patchedSources = sourcesRes.sources;
|
|
7879
7988
|
if ((((_patchOps$patches = patchOps.patches) === null || _patchOps$patches === void 0 ? void 0 : _patchOps$patches.length) ?? 0) > 0) {
|
|
7880
7989
|
const onlyPatchedTreeModules = await serverOps.getSources({
|
|
@@ -7895,7 +8004,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
7895
8004
|
};
|
|
7896
8005
|
}
|
|
7897
8006
|
}
|
|
7898
|
-
const
|
|
8007
|
+
const previewRes = await serverOps.getPreviews(schemasRes, patchedSources);
|
|
7899
8008
|
let sourcesValidation = {
|
|
7900
8009
|
errors: {},
|
|
7901
8010
|
files: {},
|
|
@@ -7946,7 +8055,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
7946
8055
|
// baseSource is only meaningful when the server applied patches:
|
|
7947
8056
|
// with apply_patches=false, `source` is already un-patched.
|
|
7948
8057
|
baseSource: applyPatches && hasPatches ? unpatchedSources[moduleFilePath] : undefined,
|
|
7949
|
-
|
|
8058
|
+
preview: previewRes.previews[moduleFilePath] || null,
|
|
7950
8059
|
patches: appliedPatches.length > 0 || skippedPatches.length > 0 || Object.keys(patchErrors).length > 0 ? {
|
|
7951
8060
|
applied: appliedPatches,
|
|
7952
8061
|
skipped: skippedPatches.length > 0 ? skippedPatches : undefined,
|
|
@@ -8139,11 +8248,65 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8139
8248
|
patchIds,
|
|
8140
8249
|
excludePatchOps: false
|
|
8141
8250
|
});
|
|
8251
|
+
/*
|
|
8252
|
+
* Exactly the patches this request consumes, and the ONLY ones it may
|
|
8253
|
+
* delete afterwards.
|
|
8254
|
+
*
|
|
8255
|
+
* Taken from what was fetched rather than from what was asked for, and
|
|
8256
|
+
* held across everything below, because the store keeps accepting writes
|
|
8257
|
+
* while this runs. A change typed during a save is not in this list, is
|
|
8258
|
+
* not applied, and must still be here when the save finishes — that is
|
|
8259
|
+
* the whole contract, and `deleteAllPatches` broke it by removing the
|
|
8260
|
+
* store wholesale.
|
|
8261
|
+
*/
|
|
8262
|
+
const consumed = patches.patches.map(patch => patch.patchId);
|
|
8142
8263
|
const analysis = serverOps.analyzePatches(patches.patches, patches.commits, commit);
|
|
8143
|
-
|
|
8264
|
+
let preparedCommit = await serverOps.prepare({
|
|
8144
8265
|
...analysis,
|
|
8145
8266
|
...patches
|
|
8146
8267
|
});
|
|
8268
|
+
/*
|
|
8269
|
+
* In `fs` mode a change that cannot be applied is removed rather than
|
|
8270
|
+
* blocking the save.
|
|
8271
|
+
*
|
|
8272
|
+
* Refusing the whole commit is right for a git commit — it is one
|
|
8273
|
+
* atomic thing, and the content api owns its own patches. It is wrong
|
|
8274
|
+
* for auto-save: the editor keeps typing, every save fails on the same
|
|
8275
|
+
* patch, and nothing is ever written again. So the failing change and
|
|
8276
|
+
* the rest of its module's chain go, the rest is written, and the person
|
|
8277
|
+
* editing is told what was thrown away.
|
|
8278
|
+
*/
|
|
8279
|
+
let removed = [];
|
|
8280
|
+
if (preparedCommit.hasErrors && serverOps instanceof ValOpsFS) {
|
|
8281
|
+
removed = computePatchesToDrop(preparedCommit);
|
|
8282
|
+
/*
|
|
8283
|
+
* Nothing to drop means nothing a second `prepare` could do better.
|
|
8284
|
+
*
|
|
8285
|
+
* `hasErrors` covers failures no patch is to blame for — a binary file
|
|
8286
|
+
* that could not be written, a module that could not be formatted, a
|
|
8287
|
+
* `.val.ts` that could not be read at all. Re-preparing every module
|
|
8288
|
+
* with an unchanged patch set would fail identically, and logging
|
|
8289
|
+
* "removed unpublished changes []" on the way would say something
|
|
8290
|
+
* untrue. It falls through to the 400 instead, which is the honest
|
|
8291
|
+
* answer: this save did not happen and the changes are still here.
|
|
8292
|
+
*/
|
|
8293
|
+
if (removed.length > 0) {
|
|
8294
|
+
const doomed = new Set(removed.map(entry => entry.patchId));
|
|
8295
|
+
const survivors = patches.patches.filter(patch => !doomed.has(patch.patchId));
|
|
8296
|
+
const survivingPatches = {
|
|
8297
|
+
...patches,
|
|
8298
|
+
patches: survivors
|
|
8299
|
+
};
|
|
8300
|
+
// One retry is enough: what is left of each module is the prefix
|
|
8301
|
+
// that already applied, and `prepare` walks modules independently,
|
|
8302
|
+
// so nothing new can fail.
|
|
8303
|
+
preparedCommit = await serverOps.prepare({
|
|
8304
|
+
...serverOps.analyzePatches(survivors, patches.commits, commit),
|
|
8305
|
+
...survivingPatches
|
|
8306
|
+
});
|
|
8307
|
+
console.error("Val: removed unpublished changes that could not be applied", removed);
|
|
8308
|
+
}
|
|
8309
|
+
}
|
|
8147
8310
|
if (preparedCommit.hasErrors) {
|
|
8148
8311
|
console.error("Failed to create commit", JSON.stringify({
|
|
8149
8312
|
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
|
@@ -8196,13 +8359,31 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8196
8359
|
}
|
|
8197
8360
|
};
|
|
8198
8361
|
}
|
|
8199
|
-
|
|
8200
|
-
|
|
8362
|
+
/*
|
|
8363
|
+
* Only what this request consumed.
|
|
8364
|
+
*
|
|
8365
|
+
* This used to be `deleteAllPatches()`, which renamed the whole store
|
|
8366
|
+
* aside and deleted it. Anything typed while the save was in flight
|
|
8367
|
+
* went with it — never applied, and indistinguishable on the client
|
|
8368
|
+
* from having been published, because both look like "the patch is
|
|
8369
|
+
* gone and the base moved". A visible edit that exists nowhere on
|
|
8370
|
+
* disk, with nothing reporting a problem.
|
|
8371
|
+
*/
|
|
8372
|
+
const deleteRes = await serverOps.deletePatches(consumed);
|
|
8373
|
+
if ("error" in deleteRes && deleteRes.error !== undefined) {
|
|
8201
8374
|
console.error(`Val got an error while cleaning up patches after publish: ${deleteRes.error.message}`);
|
|
8375
|
+
} else if ("errors" in deleteRes && deleteRes.errors !== undefined) {
|
|
8376
|
+
// Per patch, and not fatal: the files are written, so the save
|
|
8377
|
+
// succeeded. A patch left behind is announced again on the next
|
|
8378
|
+
// stat, applies to a base that already contains it, and is removed
|
|
8379
|
+
// then.
|
|
8380
|
+
console.error("Val: could not clean up some patches after publish", deleteRes.errors);
|
|
8202
8381
|
}
|
|
8203
8382
|
return {
|
|
8204
8383
|
status: 200,
|
|
8205
|
-
json:
|
|
8384
|
+
json: removed.length > 0 ? {
|
|
8385
|
+
removed
|
|
8386
|
+
} : {}
|
|
8206
8387
|
};
|
|
8207
8388
|
} else if (serverOps instanceof ValOpsHttp) {
|
|
8208
8389
|
if (auth.error === undefined && auth.id) {
|
|
@@ -2045,24 +2045,27 @@ class ValOps {
|
|
|
2045
2045
|
};
|
|
2046
2046
|
}
|
|
2047
2047
|
|
|
2048
|
-
// #region
|
|
2048
|
+
// #region getPreviews
|
|
2049
2049
|
/**
|
|
2050
|
-
* Reifies each module's
|
|
2050
|
+
* Reifies each module's previews from its schema INSTANCE.
|
|
2051
2051
|
*
|
|
2052
|
-
* Kept even though the Studio also computes
|
|
2053
|
-
* user function that lives on the instance and is not part of the serialized
|
|
2052
|
+
* Kept even though the Studio also computes previews client-side: a preview is
|
|
2053
|
+
* a user function that lives on the instance and is not part of the serialized
|
|
2054
2054
|
* schema, so a host app that does not render `<ValModulesClient>` has no
|
|
2055
|
-
* instances in the browser and would otherwise get no
|
|
2055
|
+
* instances in the browser and would otherwise get no previews at all. See
|
|
2056
2056
|
* #470.
|
|
2057
|
+
*
|
|
2058
|
+
* A string's `render` needs none of this — it is static config that travels
|
|
2059
|
+
* with the serialized schema.
|
|
2057
2060
|
*/
|
|
2058
|
-
async
|
|
2059
|
-
const
|
|
2061
|
+
async getPreviews(schemas, sources) {
|
|
2062
|
+
const previews = {};
|
|
2060
2063
|
for (const [pathS, schema] of Object.entries(schemas)) {
|
|
2061
2064
|
const path = pathS;
|
|
2062
|
-
|
|
2065
|
+
previews[path] = schema["executePreview"](path, sources[path]);
|
|
2063
2066
|
}
|
|
2064
2067
|
return {
|
|
2065
|
-
|
|
2068
|
+
previews
|
|
2066
2069
|
};
|
|
2067
2070
|
}
|
|
2068
2071
|
|
|
@@ -5442,6 +5445,112 @@ class FSOpsHost {
|
|
|
5442
5445
|
}
|
|
5443
5446
|
}
|
|
5444
5447
|
|
|
5448
|
+
/**
|
|
5449
|
+
* Which unpublished changes a failed save has to throw away to make progress.
|
|
5450
|
+
*
|
|
5451
|
+
* `/save` used to refuse the whole commit when any one patch could not be
|
|
5452
|
+
* applied: nothing was written, for any module, and the change stayed on disk to
|
|
5453
|
+
* fail again on the next attempt. With auto-save that is not a refusal, it is a
|
|
5454
|
+
* dead stop — the editor keeps typing, every save fails on the same patch, and
|
|
5455
|
+
* nothing is ever written again.
|
|
5456
|
+
*
|
|
5457
|
+
* So in `fs` mode the failing change is removed instead, and the person editing
|
|
5458
|
+
* is told. The rule for what goes with it:
|
|
5459
|
+
*
|
|
5460
|
+
* **From the first failure onward, within that module.** A patch is a set of ops
|
|
5461
|
+
* against the source as the patches before it left it. Once one has not been
|
|
5462
|
+
* applied, every later patch in that module was computed against a state that no
|
|
5463
|
+
* longer exists, so keeping them would be keeping changes whose meaning has
|
|
5464
|
+
* already been lost. That is `triedPatches` (the one that failed) plus
|
|
5465
|
+
* `skippedPatches` (the tail that was never attempted).
|
|
5466
|
+
*
|
|
5467
|
+
* **Other modules are untouched.** `prepare` walks each module's chain
|
|
5468
|
+
* independently, so a broken chain in one file says nothing about another.
|
|
5469
|
+
*
|
|
5470
|
+
* **And a module that failed as a WHOLE keeps everything.** When the `.val.ts`
|
|
5471
|
+
* cannot be read at all, `prepare` reports every one of its patches as skipped
|
|
5472
|
+
* and names none of them as unappliable — the file is what failed, not the
|
|
5473
|
+
* changes. Deleting them there would be destroying good edits because a file was
|
|
5474
|
+
* missing for a moment: rename a module, or switch branch, with unsaved changes
|
|
5475
|
+
* to it and the next auto-save would silently take them all. Skipped is only
|
|
5476
|
+
* collateral when there is something in that module for it to be collateral OF.
|
|
5477
|
+
*/
|
|
5478
|
+
|
|
5479
|
+
/**
|
|
5480
|
+
* The message for a patch that did not itself fail but sits behind one that did.
|
|
5481
|
+
*
|
|
5482
|
+
* Worth distinguishing: "yours was broken" and "something before yours was
|
|
5483
|
+
* broken" are different things to be told, and only the first is a bug in the
|
|
5484
|
+
* change itself.
|
|
5485
|
+
*/
|
|
5486
|
+
function collateralMessage(moduleFilePath) {
|
|
5487
|
+
return `An earlier change to ${moduleFilePath} could not be applied, so this one no longer fits the content it was written against.`;
|
|
5488
|
+
}
|
|
5489
|
+
function computePatchesToDrop(prepared) {
|
|
5490
|
+
const dropped = [];
|
|
5491
|
+
const seen = new Set();
|
|
5492
|
+
const add = (moduleFilePath, patchId) => {
|
|
5493
|
+
var _prepared$unappliable;
|
|
5494
|
+
if (seen.has(patchId)) {
|
|
5495
|
+
return;
|
|
5496
|
+
}
|
|
5497
|
+
seen.add(patchId);
|
|
5498
|
+
dropped.push({
|
|
5499
|
+
patchId,
|
|
5500
|
+
moduleFilePath,
|
|
5501
|
+
message: ((_prepared$unappliable = prepared.unappliablePatches[patchId]) === null || _prepared$unappliable === void 0 ? void 0 : _prepared$unappliable.message) ?? collateralMessage(moduleFilePath)
|
|
5502
|
+
});
|
|
5503
|
+
};
|
|
5504
|
+
|
|
5505
|
+
// Tried before skipped, so the change that actually failed is named first —
|
|
5506
|
+
// it is the one worth reading.
|
|
5507
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5508
|
+
for (const patchId of patchIds) {
|
|
5509
|
+
add(moduleFilePath, patchId);
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
/*
|
|
5513
|
+
* Which modules actually had a change fail, as opposed to being unreadable.
|
|
5514
|
+
*
|
|
5515
|
+
* `triedPatches` holds only failures, and `unappliablePatches` names the
|
|
5516
|
+
* failure itself, so between them they are the whole answer. A module in
|
|
5517
|
+
* neither has nothing to blame its skipped tail on.
|
|
5518
|
+
*/
|
|
5519
|
+
const failedModules = new Set();
|
|
5520
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5521
|
+
if (patchIds.length > 0) {
|
|
5522
|
+
failedModules.add(moduleFilePath);
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
for (const failure of Object.values(prepared.unappliablePatches)) {
|
|
5526
|
+
failedModules.add(failure.moduleFilePath);
|
|
5527
|
+
}
|
|
5528
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.skippedPatches)) {
|
|
5529
|
+
if (!failedModules.has(moduleFilePath)) {
|
|
5530
|
+
// The module failed as a whole. Its changes are fine and are kept, so the
|
|
5531
|
+
// save refuses instead — which is what it did before auto-save, and is
|
|
5532
|
+
// right here: this one resolves itself when the file comes back.
|
|
5533
|
+
continue;
|
|
5534
|
+
}
|
|
5535
|
+
for (const patchId of patchIds) {
|
|
5536
|
+
add(moduleFilePath, patchId);
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
|
|
5540
|
+
/*
|
|
5541
|
+
* A patch reported unappliable that neither list holds.
|
|
5542
|
+
*
|
|
5543
|
+
* Belt and braces: the two lists are how `prepare` reports the shape of the
|
|
5544
|
+
* failure, and this is how it reports the failure itself. If they ever
|
|
5545
|
+
* disagree, leaving a patch behind means the next save fails on it again —
|
|
5546
|
+
* the exact dead stop this function exists to prevent.
|
|
5547
|
+
*/
|
|
5548
|
+
for (const [patchId, failure] of Object.entries(prepared.unappliablePatches)) {
|
|
5549
|
+
add(failure.moduleFilePath, patchId);
|
|
5550
|
+
}
|
|
5551
|
+
return dropped;
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5445
5554
|
const textEncoder$2 = new TextEncoder();
|
|
5446
5555
|
const PatchId = z.z.string().refine(s => !!s); // TODO: validate
|
|
5447
5556
|
const CommitSha = z.z.string().refine(s => !!s); // TODO: validate
|
|
@@ -7870,11 +7979,11 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
7870
7979
|
// The studio client always passes false: it owns patch application
|
|
7871
7980
|
// and validation, and treats /sources/~ as a pure un-patched read.
|
|
7872
7981
|
const applyPatches = query.apply_patches !== false;
|
|
7873
|
-
// NOTE:
|
|
7874
|
-
// itself: the
|
|
7875
|
-
//
|
|
7876
|
-
//
|
|
7877
|
-
//
|
|
7982
|
+
// NOTE: previews are computed here even when the client applies patches
|
|
7983
|
+
// itself: the preview functions live on the Schema instances and are not
|
|
7984
|
+
// part of the serialized schema, so the client cannot derive them. They
|
|
7985
|
+
// are computed on the patched sources, so that previews (list titles /
|
|
7986
|
+
// subtitles / images) reflect the patches that apply.
|
|
7878
7987
|
let patchedSources = sourcesRes.sources;
|
|
7879
7988
|
if ((((_patchOps$patches = patchOps.patches) === null || _patchOps$patches === void 0 ? void 0 : _patchOps$patches.length) ?? 0) > 0) {
|
|
7880
7989
|
const onlyPatchedTreeModules = await serverOps.getSources({
|
|
@@ -7895,7 +8004,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
7895
8004
|
};
|
|
7896
8005
|
}
|
|
7897
8006
|
}
|
|
7898
|
-
const
|
|
8007
|
+
const previewRes = await serverOps.getPreviews(schemasRes, patchedSources);
|
|
7899
8008
|
let sourcesValidation = {
|
|
7900
8009
|
errors: {},
|
|
7901
8010
|
files: {},
|
|
@@ -7946,7 +8055,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
7946
8055
|
// baseSource is only meaningful when the server applied patches:
|
|
7947
8056
|
// with apply_patches=false, `source` is already un-patched.
|
|
7948
8057
|
baseSource: applyPatches && hasPatches ? unpatchedSources[moduleFilePath] : undefined,
|
|
7949
|
-
|
|
8058
|
+
preview: previewRes.previews[moduleFilePath] || null,
|
|
7950
8059
|
patches: appliedPatches.length > 0 || skippedPatches.length > 0 || Object.keys(patchErrors).length > 0 ? {
|
|
7951
8060
|
applied: appliedPatches,
|
|
7952
8061
|
skipped: skippedPatches.length > 0 ? skippedPatches : undefined,
|
|
@@ -8139,11 +8248,65 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8139
8248
|
patchIds,
|
|
8140
8249
|
excludePatchOps: false
|
|
8141
8250
|
});
|
|
8251
|
+
/*
|
|
8252
|
+
* Exactly the patches this request consumes, and the ONLY ones it may
|
|
8253
|
+
* delete afterwards.
|
|
8254
|
+
*
|
|
8255
|
+
* Taken from what was fetched rather than from what was asked for, and
|
|
8256
|
+
* held across everything below, because the store keeps accepting writes
|
|
8257
|
+
* while this runs. A change typed during a save is not in this list, is
|
|
8258
|
+
* not applied, and must still be here when the save finishes — that is
|
|
8259
|
+
* the whole contract, and `deleteAllPatches` broke it by removing the
|
|
8260
|
+
* store wholesale.
|
|
8261
|
+
*/
|
|
8262
|
+
const consumed = patches.patches.map(patch => patch.patchId);
|
|
8142
8263
|
const analysis = serverOps.analyzePatches(patches.patches, patches.commits, commit);
|
|
8143
|
-
|
|
8264
|
+
let preparedCommit = await serverOps.prepare({
|
|
8144
8265
|
...analysis,
|
|
8145
8266
|
...patches
|
|
8146
8267
|
});
|
|
8268
|
+
/*
|
|
8269
|
+
* In `fs` mode a change that cannot be applied is removed rather than
|
|
8270
|
+
* blocking the save.
|
|
8271
|
+
*
|
|
8272
|
+
* Refusing the whole commit is right for a git commit — it is one
|
|
8273
|
+
* atomic thing, and the content api owns its own patches. It is wrong
|
|
8274
|
+
* for auto-save: the editor keeps typing, every save fails on the same
|
|
8275
|
+
* patch, and nothing is ever written again. So the failing change and
|
|
8276
|
+
* the rest of its module's chain go, the rest is written, and the person
|
|
8277
|
+
* editing is told what was thrown away.
|
|
8278
|
+
*/
|
|
8279
|
+
let removed = [];
|
|
8280
|
+
if (preparedCommit.hasErrors && serverOps instanceof ValOpsFS) {
|
|
8281
|
+
removed = computePatchesToDrop(preparedCommit);
|
|
8282
|
+
/*
|
|
8283
|
+
* Nothing to drop means nothing a second `prepare` could do better.
|
|
8284
|
+
*
|
|
8285
|
+
* `hasErrors` covers failures no patch is to blame for — a binary file
|
|
8286
|
+
* that could not be written, a module that could not be formatted, a
|
|
8287
|
+
* `.val.ts` that could not be read at all. Re-preparing every module
|
|
8288
|
+
* with an unchanged patch set would fail identically, and logging
|
|
8289
|
+
* "removed unpublished changes []" on the way would say something
|
|
8290
|
+
* untrue. It falls through to the 400 instead, which is the honest
|
|
8291
|
+
* answer: this save did not happen and the changes are still here.
|
|
8292
|
+
*/
|
|
8293
|
+
if (removed.length > 0) {
|
|
8294
|
+
const doomed = new Set(removed.map(entry => entry.patchId));
|
|
8295
|
+
const survivors = patches.patches.filter(patch => !doomed.has(patch.patchId));
|
|
8296
|
+
const survivingPatches = {
|
|
8297
|
+
...patches,
|
|
8298
|
+
patches: survivors
|
|
8299
|
+
};
|
|
8300
|
+
// One retry is enough: what is left of each module is the prefix
|
|
8301
|
+
// that already applied, and `prepare` walks modules independently,
|
|
8302
|
+
// so nothing new can fail.
|
|
8303
|
+
preparedCommit = await serverOps.prepare({
|
|
8304
|
+
...serverOps.analyzePatches(survivors, patches.commits, commit),
|
|
8305
|
+
...survivingPatches
|
|
8306
|
+
});
|
|
8307
|
+
console.error("Val: removed unpublished changes that could not be applied", removed);
|
|
8308
|
+
}
|
|
8309
|
+
}
|
|
8147
8310
|
if (preparedCommit.hasErrors) {
|
|
8148
8311
|
console.error("Failed to create commit", JSON.stringify({
|
|
8149
8312
|
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
|
@@ -8196,13 +8359,31 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8196
8359
|
}
|
|
8197
8360
|
};
|
|
8198
8361
|
}
|
|
8199
|
-
|
|
8200
|
-
|
|
8362
|
+
/*
|
|
8363
|
+
* Only what this request consumed.
|
|
8364
|
+
*
|
|
8365
|
+
* This used to be `deleteAllPatches()`, which renamed the whole store
|
|
8366
|
+
* aside and deleted it. Anything typed while the save was in flight
|
|
8367
|
+
* went with it — never applied, and indistinguishable on the client
|
|
8368
|
+
* from having been published, because both look like "the patch is
|
|
8369
|
+
* gone and the base moved". A visible edit that exists nowhere on
|
|
8370
|
+
* disk, with nothing reporting a problem.
|
|
8371
|
+
*/
|
|
8372
|
+
const deleteRes = await serverOps.deletePatches(consumed);
|
|
8373
|
+
if ("error" in deleteRes && deleteRes.error !== undefined) {
|
|
8201
8374
|
console.error(`Val got an error while cleaning up patches after publish: ${deleteRes.error.message}`);
|
|
8375
|
+
} else if ("errors" in deleteRes && deleteRes.errors !== undefined) {
|
|
8376
|
+
// Per patch, and not fatal: the files are written, so the save
|
|
8377
|
+
// succeeded. A patch left behind is announced again on the next
|
|
8378
|
+
// stat, applies to a base that already contains it, and is removed
|
|
8379
|
+
// then.
|
|
8380
|
+
console.error("Val: could not clean up some patches after publish", deleteRes.errors);
|
|
8202
8381
|
}
|
|
8203
8382
|
return {
|
|
8204
8383
|
status: 200,
|
|
8205
|
-
json:
|
|
8384
|
+
json: removed.length > 0 ? {
|
|
8385
|
+
removed
|
|
8386
|
+
} : {}
|
|
8206
8387
|
};
|
|
8207
8388
|
} else if (serverOps instanceof ValOpsHttp) {
|
|
8208
8389
|
if (auth.error === undefined && auth.id) {
|
|
@@ -2011,24 +2011,27 @@ class ValOps {
|
|
|
2011
2011
|
};
|
|
2012
2012
|
}
|
|
2013
2013
|
|
|
2014
|
-
// #region
|
|
2014
|
+
// #region getPreviews
|
|
2015
2015
|
/**
|
|
2016
|
-
* Reifies each module's
|
|
2016
|
+
* Reifies each module's previews from its schema INSTANCE.
|
|
2017
2017
|
*
|
|
2018
|
-
* Kept even though the Studio also computes
|
|
2019
|
-
* user function that lives on the instance and is not part of the serialized
|
|
2018
|
+
* Kept even though the Studio also computes previews client-side: a preview is
|
|
2019
|
+
* a user function that lives on the instance and is not part of the serialized
|
|
2020
2020
|
* schema, so a host app that does not render `<ValModulesClient>` has no
|
|
2021
|
-
* instances in the browser and would otherwise get no
|
|
2021
|
+
* instances in the browser and would otherwise get no previews at all. See
|
|
2022
2022
|
* #470.
|
|
2023
|
+
*
|
|
2024
|
+
* A string's `render` needs none of this — it is static config that travels
|
|
2025
|
+
* with the serialized schema.
|
|
2023
2026
|
*/
|
|
2024
|
-
async
|
|
2025
|
-
const
|
|
2027
|
+
async getPreviews(schemas, sources) {
|
|
2028
|
+
const previews = {};
|
|
2026
2029
|
for (const [pathS, schema] of Object.entries(schemas)) {
|
|
2027
2030
|
const path = pathS;
|
|
2028
|
-
|
|
2031
|
+
previews[path] = schema["executePreview"](path, sources[path]);
|
|
2029
2032
|
}
|
|
2030
2033
|
return {
|
|
2031
|
-
|
|
2034
|
+
previews
|
|
2032
2035
|
};
|
|
2033
2036
|
}
|
|
2034
2037
|
|
|
@@ -5408,6 +5411,112 @@ class FSOpsHost {
|
|
|
5408
5411
|
}
|
|
5409
5412
|
}
|
|
5410
5413
|
|
|
5414
|
+
/**
|
|
5415
|
+
* Which unpublished changes a failed save has to throw away to make progress.
|
|
5416
|
+
*
|
|
5417
|
+
* `/save` used to refuse the whole commit when any one patch could not be
|
|
5418
|
+
* applied: nothing was written, for any module, and the change stayed on disk to
|
|
5419
|
+
* fail again on the next attempt. With auto-save that is not a refusal, it is a
|
|
5420
|
+
* dead stop — the editor keeps typing, every save fails on the same patch, and
|
|
5421
|
+
* nothing is ever written again.
|
|
5422
|
+
*
|
|
5423
|
+
* So in `fs` mode the failing change is removed instead, and the person editing
|
|
5424
|
+
* is told. The rule for what goes with it:
|
|
5425
|
+
*
|
|
5426
|
+
* **From the first failure onward, within that module.** A patch is a set of ops
|
|
5427
|
+
* against the source as the patches before it left it. Once one has not been
|
|
5428
|
+
* applied, every later patch in that module was computed against a state that no
|
|
5429
|
+
* longer exists, so keeping them would be keeping changes whose meaning has
|
|
5430
|
+
* already been lost. That is `triedPatches` (the one that failed) plus
|
|
5431
|
+
* `skippedPatches` (the tail that was never attempted).
|
|
5432
|
+
*
|
|
5433
|
+
* **Other modules are untouched.** `prepare` walks each module's chain
|
|
5434
|
+
* independently, so a broken chain in one file says nothing about another.
|
|
5435
|
+
*
|
|
5436
|
+
* **And a module that failed as a WHOLE keeps everything.** When the `.val.ts`
|
|
5437
|
+
* cannot be read at all, `prepare` reports every one of its patches as skipped
|
|
5438
|
+
* and names none of them as unappliable — the file is what failed, not the
|
|
5439
|
+
* changes. Deleting them there would be destroying good edits because a file was
|
|
5440
|
+
* missing for a moment: rename a module, or switch branch, with unsaved changes
|
|
5441
|
+
* to it and the next auto-save would silently take them all. Skipped is only
|
|
5442
|
+
* collateral when there is something in that module for it to be collateral OF.
|
|
5443
|
+
*/
|
|
5444
|
+
|
|
5445
|
+
/**
|
|
5446
|
+
* The message for a patch that did not itself fail but sits behind one that did.
|
|
5447
|
+
*
|
|
5448
|
+
* Worth distinguishing: "yours was broken" and "something before yours was
|
|
5449
|
+
* broken" are different things to be told, and only the first is a bug in the
|
|
5450
|
+
* change itself.
|
|
5451
|
+
*/
|
|
5452
|
+
function collateralMessage(moduleFilePath) {
|
|
5453
|
+
return `An earlier change to ${moduleFilePath} could not be applied, so this one no longer fits the content it was written against.`;
|
|
5454
|
+
}
|
|
5455
|
+
function computePatchesToDrop(prepared) {
|
|
5456
|
+
const dropped = [];
|
|
5457
|
+
const seen = new Set();
|
|
5458
|
+
const add = (moduleFilePath, patchId) => {
|
|
5459
|
+
var _prepared$unappliable;
|
|
5460
|
+
if (seen.has(patchId)) {
|
|
5461
|
+
return;
|
|
5462
|
+
}
|
|
5463
|
+
seen.add(patchId);
|
|
5464
|
+
dropped.push({
|
|
5465
|
+
patchId,
|
|
5466
|
+
moduleFilePath,
|
|
5467
|
+
message: ((_prepared$unappliable = prepared.unappliablePatches[patchId]) === null || _prepared$unappliable === void 0 ? void 0 : _prepared$unappliable.message) ?? collateralMessage(moduleFilePath)
|
|
5468
|
+
});
|
|
5469
|
+
};
|
|
5470
|
+
|
|
5471
|
+
// Tried before skipped, so the change that actually failed is named first —
|
|
5472
|
+
// it is the one worth reading.
|
|
5473
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5474
|
+
for (const patchId of patchIds) {
|
|
5475
|
+
add(moduleFilePath, patchId);
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
/*
|
|
5479
|
+
* Which modules actually had a change fail, as opposed to being unreadable.
|
|
5480
|
+
*
|
|
5481
|
+
* `triedPatches` holds only failures, and `unappliablePatches` names the
|
|
5482
|
+
* failure itself, so between them they are the whole answer. A module in
|
|
5483
|
+
* neither has nothing to blame its skipped tail on.
|
|
5484
|
+
*/
|
|
5485
|
+
const failedModules = new Set();
|
|
5486
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5487
|
+
if (patchIds.length > 0) {
|
|
5488
|
+
failedModules.add(moduleFilePath);
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
for (const failure of Object.values(prepared.unappliablePatches)) {
|
|
5492
|
+
failedModules.add(failure.moduleFilePath);
|
|
5493
|
+
}
|
|
5494
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.skippedPatches)) {
|
|
5495
|
+
if (!failedModules.has(moduleFilePath)) {
|
|
5496
|
+
// The module failed as a whole. Its changes are fine and are kept, so the
|
|
5497
|
+
// save refuses instead — which is what it did before auto-save, and is
|
|
5498
|
+
// right here: this one resolves itself when the file comes back.
|
|
5499
|
+
continue;
|
|
5500
|
+
}
|
|
5501
|
+
for (const patchId of patchIds) {
|
|
5502
|
+
add(moduleFilePath, patchId);
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
|
|
5506
|
+
/*
|
|
5507
|
+
* A patch reported unappliable that neither list holds.
|
|
5508
|
+
*
|
|
5509
|
+
* Belt and braces: the two lists are how `prepare` reports the shape of the
|
|
5510
|
+
* failure, and this is how it reports the failure itself. If they ever
|
|
5511
|
+
* disagree, leaving a patch behind means the next save fails on it again —
|
|
5512
|
+
* the exact dead stop this function exists to prevent.
|
|
5513
|
+
*/
|
|
5514
|
+
for (const [patchId, failure] of Object.entries(prepared.unappliablePatches)) {
|
|
5515
|
+
add(failure.moduleFilePath, patchId);
|
|
5516
|
+
}
|
|
5517
|
+
return dropped;
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5411
5520
|
const textEncoder$2 = new TextEncoder();
|
|
5412
5521
|
const PatchId = z.string().refine(s => !!s); // TODO: validate
|
|
5413
5522
|
const CommitSha = z.string().refine(s => !!s); // TODO: validate
|
|
@@ -7836,11 +7945,11 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
7836
7945
|
// The studio client always passes false: it owns patch application
|
|
7837
7946
|
// and validation, and treats /sources/~ as a pure un-patched read.
|
|
7838
7947
|
const applyPatches = query.apply_patches !== false;
|
|
7839
|
-
// NOTE:
|
|
7840
|
-
// itself: the
|
|
7841
|
-
//
|
|
7842
|
-
//
|
|
7843
|
-
//
|
|
7948
|
+
// NOTE: previews are computed here even when the client applies patches
|
|
7949
|
+
// itself: the preview functions live on the Schema instances and are not
|
|
7950
|
+
// part of the serialized schema, so the client cannot derive them. They
|
|
7951
|
+
// are computed on the patched sources, so that previews (list titles /
|
|
7952
|
+
// subtitles / images) reflect the patches that apply.
|
|
7844
7953
|
let patchedSources = sourcesRes.sources;
|
|
7845
7954
|
if ((((_patchOps$patches = patchOps.patches) === null || _patchOps$patches === void 0 ? void 0 : _patchOps$patches.length) ?? 0) > 0) {
|
|
7846
7955
|
const onlyPatchedTreeModules = await serverOps.getSources({
|
|
@@ -7861,7 +7970,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
7861
7970
|
};
|
|
7862
7971
|
}
|
|
7863
7972
|
}
|
|
7864
|
-
const
|
|
7973
|
+
const previewRes = await serverOps.getPreviews(schemasRes, patchedSources);
|
|
7865
7974
|
let sourcesValidation = {
|
|
7866
7975
|
errors: {},
|
|
7867
7976
|
files: {},
|
|
@@ -7912,7 +8021,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
7912
8021
|
// baseSource is only meaningful when the server applied patches:
|
|
7913
8022
|
// with apply_patches=false, `source` is already un-patched.
|
|
7914
8023
|
baseSource: applyPatches && hasPatches ? unpatchedSources[moduleFilePath] : undefined,
|
|
7915
|
-
|
|
8024
|
+
preview: previewRes.previews[moduleFilePath] || null,
|
|
7916
8025
|
patches: appliedPatches.length > 0 || skippedPatches.length > 0 || Object.keys(patchErrors).length > 0 ? {
|
|
7917
8026
|
applied: appliedPatches,
|
|
7918
8027
|
skipped: skippedPatches.length > 0 ? skippedPatches : undefined,
|
|
@@ -8105,11 +8214,65 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8105
8214
|
patchIds,
|
|
8106
8215
|
excludePatchOps: false
|
|
8107
8216
|
});
|
|
8217
|
+
/*
|
|
8218
|
+
* Exactly the patches this request consumes, and the ONLY ones it may
|
|
8219
|
+
* delete afterwards.
|
|
8220
|
+
*
|
|
8221
|
+
* Taken from what was fetched rather than from what was asked for, and
|
|
8222
|
+
* held across everything below, because the store keeps accepting writes
|
|
8223
|
+
* while this runs. A change typed during a save is not in this list, is
|
|
8224
|
+
* not applied, and must still be here when the save finishes — that is
|
|
8225
|
+
* the whole contract, and `deleteAllPatches` broke it by removing the
|
|
8226
|
+
* store wholesale.
|
|
8227
|
+
*/
|
|
8228
|
+
const consumed = patches.patches.map(patch => patch.patchId);
|
|
8108
8229
|
const analysis = serverOps.analyzePatches(patches.patches, patches.commits, commit);
|
|
8109
|
-
|
|
8230
|
+
let preparedCommit = await serverOps.prepare({
|
|
8110
8231
|
...analysis,
|
|
8111
8232
|
...patches
|
|
8112
8233
|
});
|
|
8234
|
+
/*
|
|
8235
|
+
* In `fs` mode a change that cannot be applied is removed rather than
|
|
8236
|
+
* blocking the save.
|
|
8237
|
+
*
|
|
8238
|
+
* Refusing the whole commit is right for a git commit — it is one
|
|
8239
|
+
* atomic thing, and the content api owns its own patches. It is wrong
|
|
8240
|
+
* for auto-save: the editor keeps typing, every save fails on the same
|
|
8241
|
+
* patch, and nothing is ever written again. So the failing change and
|
|
8242
|
+
* the rest of its module's chain go, the rest is written, and the person
|
|
8243
|
+
* editing is told what was thrown away.
|
|
8244
|
+
*/
|
|
8245
|
+
let removed = [];
|
|
8246
|
+
if (preparedCommit.hasErrors && serverOps instanceof ValOpsFS) {
|
|
8247
|
+
removed = computePatchesToDrop(preparedCommit);
|
|
8248
|
+
/*
|
|
8249
|
+
* Nothing to drop means nothing a second `prepare` could do better.
|
|
8250
|
+
*
|
|
8251
|
+
* `hasErrors` covers failures no patch is to blame for — a binary file
|
|
8252
|
+
* that could not be written, a module that could not be formatted, a
|
|
8253
|
+
* `.val.ts` that could not be read at all. Re-preparing every module
|
|
8254
|
+
* with an unchanged patch set would fail identically, and logging
|
|
8255
|
+
* "removed unpublished changes []" on the way would say something
|
|
8256
|
+
* untrue. It falls through to the 400 instead, which is the honest
|
|
8257
|
+
* answer: this save did not happen and the changes are still here.
|
|
8258
|
+
*/
|
|
8259
|
+
if (removed.length > 0) {
|
|
8260
|
+
const doomed = new Set(removed.map(entry => entry.patchId));
|
|
8261
|
+
const survivors = patches.patches.filter(patch => !doomed.has(patch.patchId));
|
|
8262
|
+
const survivingPatches = {
|
|
8263
|
+
...patches,
|
|
8264
|
+
patches: survivors
|
|
8265
|
+
};
|
|
8266
|
+
// One retry is enough: what is left of each module is the prefix
|
|
8267
|
+
// that already applied, and `prepare` walks modules independently,
|
|
8268
|
+
// so nothing new can fail.
|
|
8269
|
+
preparedCommit = await serverOps.prepare({
|
|
8270
|
+
...serverOps.analyzePatches(survivors, patches.commits, commit),
|
|
8271
|
+
...survivingPatches
|
|
8272
|
+
});
|
|
8273
|
+
console.error("Val: removed unpublished changes that could not be applied", removed);
|
|
8274
|
+
}
|
|
8275
|
+
}
|
|
8113
8276
|
if (preparedCommit.hasErrors) {
|
|
8114
8277
|
console.error("Failed to create commit", JSON.stringify({
|
|
8115
8278
|
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
|
@@ -8162,13 +8325,31 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8162
8325
|
}
|
|
8163
8326
|
};
|
|
8164
8327
|
}
|
|
8165
|
-
|
|
8166
|
-
|
|
8328
|
+
/*
|
|
8329
|
+
* Only what this request consumed.
|
|
8330
|
+
*
|
|
8331
|
+
* This used to be `deleteAllPatches()`, which renamed the whole store
|
|
8332
|
+
* aside and deleted it. Anything typed while the save was in flight
|
|
8333
|
+
* went with it — never applied, and indistinguishable on the client
|
|
8334
|
+
* from having been published, because both look like "the patch is
|
|
8335
|
+
* gone and the base moved". A visible edit that exists nowhere on
|
|
8336
|
+
* disk, with nothing reporting a problem.
|
|
8337
|
+
*/
|
|
8338
|
+
const deleteRes = await serverOps.deletePatches(consumed);
|
|
8339
|
+
if ("error" in deleteRes && deleteRes.error !== undefined) {
|
|
8167
8340
|
console.error(`Val got an error while cleaning up patches after publish: ${deleteRes.error.message}`);
|
|
8341
|
+
} else if ("errors" in deleteRes && deleteRes.errors !== undefined) {
|
|
8342
|
+
// Per patch, and not fatal: the files are written, so the save
|
|
8343
|
+
// succeeded. A patch left behind is announced again on the next
|
|
8344
|
+
// stat, applies to a base that already contains it, and is removed
|
|
8345
|
+
// then.
|
|
8346
|
+
console.error("Val: could not clean up some patches after publish", deleteRes.errors);
|
|
8168
8347
|
}
|
|
8169
8348
|
return {
|
|
8170
8349
|
status: 200,
|
|
8171
|
-
json:
|
|
8350
|
+
json: removed.length > 0 ? {
|
|
8351
|
+
removed
|
|
8352
|
+
} : {}
|
|
8172
8353
|
};
|
|
8173
8354
|
} else if (serverOps instanceof ValOpsHttp) {
|
|
8174
8355
|
if (auth.error === undefined && auth.id) {
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"./package.json": "./package.json"
|
|
17
17
|
},
|
|
18
18
|
"types": "dist/valbuild-server.cjs.d.ts",
|
|
19
|
-
"version": "0.
|
|
19
|
+
"version": "0.105.0",
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@prettier/sync": "^0.6.1",
|
|
22
22
|
"@types/jest": "^30.0.0"
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"typescript": "^5.9.3",
|
|
30
30
|
"zod": "^4.3.5",
|
|
31
31
|
"zod-validation-error": "^5.0.0",
|
|
32
|
-
"@valbuild/core": "0.
|
|
33
|
-
"@valbuild/shared": "0.
|
|
34
|
-
"@valbuild/ui": "0.
|
|
32
|
+
"@valbuild/core": "0.105.0",
|
|
33
|
+
"@valbuild/shared": "0.105.0",
|
|
34
|
+
"@valbuild/ui": "0.105.0"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=18.17.0"
|