@valbuild/server 0.103.1 → 0.103.2
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.
|
@@ -5442,6 +5442,112 @@ class FSOpsHost {
|
|
|
5442
5442
|
}
|
|
5443
5443
|
}
|
|
5444
5444
|
|
|
5445
|
+
/**
|
|
5446
|
+
* Which unpublished changes a failed save has to throw away to make progress.
|
|
5447
|
+
*
|
|
5448
|
+
* `/save` used to refuse the whole commit when any one patch could not be
|
|
5449
|
+
* applied: nothing was written, for any module, and the change stayed on disk to
|
|
5450
|
+
* fail again on the next attempt. With auto-save that is not a refusal, it is a
|
|
5451
|
+
* dead stop — the editor keeps typing, every save fails on the same patch, and
|
|
5452
|
+
* nothing is ever written again.
|
|
5453
|
+
*
|
|
5454
|
+
* So in `fs` mode the failing change is removed instead, and the person editing
|
|
5455
|
+
* is told. The rule for what goes with it:
|
|
5456
|
+
*
|
|
5457
|
+
* **From the first failure onward, within that module.** A patch is a set of ops
|
|
5458
|
+
* against the source as the patches before it left it. Once one has not been
|
|
5459
|
+
* applied, every later patch in that module was computed against a state that no
|
|
5460
|
+
* longer exists, so keeping them would be keeping changes whose meaning has
|
|
5461
|
+
* already been lost. That is `triedPatches` (the one that failed) plus
|
|
5462
|
+
* `skippedPatches` (the tail that was never attempted).
|
|
5463
|
+
*
|
|
5464
|
+
* **Other modules are untouched.** `prepare` walks each module's chain
|
|
5465
|
+
* independently, so a broken chain in one file says nothing about another.
|
|
5466
|
+
*
|
|
5467
|
+
* **And a module that failed as a WHOLE keeps everything.** When the `.val.ts`
|
|
5468
|
+
* cannot be read at all, `prepare` reports every one of its patches as skipped
|
|
5469
|
+
* and names none of them as unappliable — the file is what failed, not the
|
|
5470
|
+
* changes. Deleting them there would be destroying good edits because a file was
|
|
5471
|
+
* missing for a moment: rename a module, or switch branch, with unsaved changes
|
|
5472
|
+
* to it and the next auto-save would silently take them all. Skipped is only
|
|
5473
|
+
* collateral when there is something in that module for it to be collateral OF.
|
|
5474
|
+
*/
|
|
5475
|
+
|
|
5476
|
+
/**
|
|
5477
|
+
* The message for a patch that did not itself fail but sits behind one that did.
|
|
5478
|
+
*
|
|
5479
|
+
* Worth distinguishing: "yours was broken" and "something before yours was
|
|
5480
|
+
* broken" are different things to be told, and only the first is a bug in the
|
|
5481
|
+
* change itself.
|
|
5482
|
+
*/
|
|
5483
|
+
function collateralMessage(moduleFilePath) {
|
|
5484
|
+
return `An earlier change to ${moduleFilePath} could not be applied, so this one no longer fits the content it was written against.`;
|
|
5485
|
+
}
|
|
5486
|
+
function computePatchesToDrop(prepared) {
|
|
5487
|
+
const dropped = [];
|
|
5488
|
+
const seen = new Set();
|
|
5489
|
+
const add = (moduleFilePath, patchId) => {
|
|
5490
|
+
var _prepared$unappliable;
|
|
5491
|
+
if (seen.has(patchId)) {
|
|
5492
|
+
return;
|
|
5493
|
+
}
|
|
5494
|
+
seen.add(patchId);
|
|
5495
|
+
dropped.push({
|
|
5496
|
+
patchId,
|
|
5497
|
+
moduleFilePath,
|
|
5498
|
+
message: ((_prepared$unappliable = prepared.unappliablePatches[patchId]) === null || _prepared$unappliable === void 0 ? void 0 : _prepared$unappliable.message) ?? collateralMessage(moduleFilePath)
|
|
5499
|
+
});
|
|
5500
|
+
};
|
|
5501
|
+
|
|
5502
|
+
// Tried before skipped, so the change that actually failed is named first —
|
|
5503
|
+
// it is the one worth reading.
|
|
5504
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5505
|
+
for (const patchId of patchIds) {
|
|
5506
|
+
add(moduleFilePath, patchId);
|
|
5507
|
+
}
|
|
5508
|
+
}
|
|
5509
|
+
/*
|
|
5510
|
+
* Which modules actually had a change fail, as opposed to being unreadable.
|
|
5511
|
+
*
|
|
5512
|
+
* `triedPatches` holds only failures, and `unappliablePatches` names the
|
|
5513
|
+
* failure itself, so between them they are the whole answer. A module in
|
|
5514
|
+
* neither has nothing to blame its skipped tail on.
|
|
5515
|
+
*/
|
|
5516
|
+
const failedModules = new Set();
|
|
5517
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5518
|
+
if (patchIds.length > 0) {
|
|
5519
|
+
failedModules.add(moduleFilePath);
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
for (const failure of Object.values(prepared.unappliablePatches)) {
|
|
5523
|
+
failedModules.add(failure.moduleFilePath);
|
|
5524
|
+
}
|
|
5525
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.skippedPatches)) {
|
|
5526
|
+
if (!failedModules.has(moduleFilePath)) {
|
|
5527
|
+
// The module failed as a whole. Its changes are fine and are kept, so the
|
|
5528
|
+
// save refuses instead — which is what it did before auto-save, and is
|
|
5529
|
+
// right here: this one resolves itself when the file comes back.
|
|
5530
|
+
continue;
|
|
5531
|
+
}
|
|
5532
|
+
for (const patchId of patchIds) {
|
|
5533
|
+
add(moduleFilePath, patchId);
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
|
|
5537
|
+
/*
|
|
5538
|
+
* A patch reported unappliable that neither list holds.
|
|
5539
|
+
*
|
|
5540
|
+
* Belt and braces: the two lists are how `prepare` reports the shape of the
|
|
5541
|
+
* failure, and this is how it reports the failure itself. If they ever
|
|
5542
|
+
* disagree, leaving a patch behind means the next save fails on it again —
|
|
5543
|
+
* the exact dead stop this function exists to prevent.
|
|
5544
|
+
*/
|
|
5545
|
+
for (const [patchId, failure] of Object.entries(prepared.unappliablePatches)) {
|
|
5546
|
+
add(failure.moduleFilePath, patchId);
|
|
5547
|
+
}
|
|
5548
|
+
return dropped;
|
|
5549
|
+
}
|
|
5550
|
+
|
|
5445
5551
|
const textEncoder$2 = new TextEncoder();
|
|
5446
5552
|
const PatchId = z.z.string().refine(s => !!s); // TODO: validate
|
|
5447
5553
|
const CommitSha = z.z.string().refine(s => !!s); // TODO: validate
|
|
@@ -8139,11 +8245,65 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8139
8245
|
patchIds,
|
|
8140
8246
|
excludePatchOps: false
|
|
8141
8247
|
});
|
|
8248
|
+
/*
|
|
8249
|
+
* Exactly the patches this request consumes, and the ONLY ones it may
|
|
8250
|
+
* delete afterwards.
|
|
8251
|
+
*
|
|
8252
|
+
* Taken from what was fetched rather than from what was asked for, and
|
|
8253
|
+
* held across everything below, because the store keeps accepting writes
|
|
8254
|
+
* while this runs. A change typed during a save is not in this list, is
|
|
8255
|
+
* not applied, and must still be here when the save finishes — that is
|
|
8256
|
+
* the whole contract, and `deleteAllPatches` broke it by removing the
|
|
8257
|
+
* store wholesale.
|
|
8258
|
+
*/
|
|
8259
|
+
const consumed = patches.patches.map(patch => patch.patchId);
|
|
8142
8260
|
const analysis = serverOps.analyzePatches(patches.patches, patches.commits, commit);
|
|
8143
|
-
|
|
8261
|
+
let preparedCommit = await serverOps.prepare({
|
|
8144
8262
|
...analysis,
|
|
8145
8263
|
...patches
|
|
8146
8264
|
});
|
|
8265
|
+
/*
|
|
8266
|
+
* In `fs` mode a change that cannot be applied is removed rather than
|
|
8267
|
+
* blocking the save.
|
|
8268
|
+
*
|
|
8269
|
+
* Refusing the whole commit is right for a git commit — it is one
|
|
8270
|
+
* atomic thing, and the content api owns its own patches. It is wrong
|
|
8271
|
+
* for auto-save: the editor keeps typing, every save fails on the same
|
|
8272
|
+
* patch, and nothing is ever written again. So the failing change and
|
|
8273
|
+
* the rest of its module's chain go, the rest is written, and the person
|
|
8274
|
+
* editing is told what was thrown away.
|
|
8275
|
+
*/
|
|
8276
|
+
let removed = [];
|
|
8277
|
+
if (preparedCommit.hasErrors && serverOps instanceof ValOpsFS) {
|
|
8278
|
+
removed = computePatchesToDrop(preparedCommit);
|
|
8279
|
+
/*
|
|
8280
|
+
* Nothing to drop means nothing a second `prepare` could do better.
|
|
8281
|
+
*
|
|
8282
|
+
* `hasErrors` covers failures no patch is to blame for — a binary file
|
|
8283
|
+
* that could not be written, a module that could not be formatted, a
|
|
8284
|
+
* `.val.ts` that could not be read at all. Re-preparing every module
|
|
8285
|
+
* with an unchanged patch set would fail identically, and logging
|
|
8286
|
+
* "removed unpublished changes []" on the way would say something
|
|
8287
|
+
* untrue. It falls through to the 400 instead, which is the honest
|
|
8288
|
+
* answer: this save did not happen and the changes are still here.
|
|
8289
|
+
*/
|
|
8290
|
+
if (removed.length > 0) {
|
|
8291
|
+
const doomed = new Set(removed.map(entry => entry.patchId));
|
|
8292
|
+
const survivors = patches.patches.filter(patch => !doomed.has(patch.patchId));
|
|
8293
|
+
const survivingPatches = {
|
|
8294
|
+
...patches,
|
|
8295
|
+
patches: survivors
|
|
8296
|
+
};
|
|
8297
|
+
// One retry is enough: what is left of each module is the prefix
|
|
8298
|
+
// that already applied, and `prepare` walks modules independently,
|
|
8299
|
+
// so nothing new can fail.
|
|
8300
|
+
preparedCommit = await serverOps.prepare({
|
|
8301
|
+
...serverOps.analyzePatches(survivors, patches.commits, commit),
|
|
8302
|
+
...survivingPatches
|
|
8303
|
+
});
|
|
8304
|
+
console.error("Val: removed unpublished changes that could not be applied", removed);
|
|
8305
|
+
}
|
|
8306
|
+
}
|
|
8147
8307
|
if (preparedCommit.hasErrors) {
|
|
8148
8308
|
console.error("Failed to create commit", JSON.stringify({
|
|
8149
8309
|
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
|
@@ -8196,13 +8356,31 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8196
8356
|
}
|
|
8197
8357
|
};
|
|
8198
8358
|
}
|
|
8199
|
-
|
|
8200
|
-
|
|
8359
|
+
/*
|
|
8360
|
+
* Only what this request consumed.
|
|
8361
|
+
*
|
|
8362
|
+
* This used to be `deleteAllPatches()`, which renamed the whole store
|
|
8363
|
+
* aside and deleted it. Anything typed while the save was in flight
|
|
8364
|
+
* went with it — never applied, and indistinguishable on the client
|
|
8365
|
+
* from having been published, because both look like "the patch is
|
|
8366
|
+
* gone and the base moved". A visible edit that exists nowhere on
|
|
8367
|
+
* disk, with nothing reporting a problem.
|
|
8368
|
+
*/
|
|
8369
|
+
const deleteRes = await serverOps.deletePatches(consumed);
|
|
8370
|
+
if ("error" in deleteRes && deleteRes.error !== undefined) {
|
|
8201
8371
|
console.error(`Val got an error while cleaning up patches after publish: ${deleteRes.error.message}`);
|
|
8372
|
+
} else if ("errors" in deleteRes && deleteRes.errors !== undefined) {
|
|
8373
|
+
// Per patch, and not fatal: the files are written, so the save
|
|
8374
|
+
// succeeded. A patch left behind is announced again on the next
|
|
8375
|
+
// stat, applies to a base that already contains it, and is removed
|
|
8376
|
+
// then.
|
|
8377
|
+
console.error("Val: could not clean up some patches after publish", deleteRes.errors);
|
|
8202
8378
|
}
|
|
8203
8379
|
return {
|
|
8204
8380
|
status: 200,
|
|
8205
|
-
json:
|
|
8381
|
+
json: removed.length > 0 ? {
|
|
8382
|
+
removed
|
|
8383
|
+
} : {}
|
|
8206
8384
|
};
|
|
8207
8385
|
} else if (serverOps instanceof ValOpsHttp) {
|
|
8208
8386
|
if (auth.error === undefined && auth.id) {
|
|
@@ -5442,6 +5442,112 @@ class FSOpsHost {
|
|
|
5442
5442
|
}
|
|
5443
5443
|
}
|
|
5444
5444
|
|
|
5445
|
+
/**
|
|
5446
|
+
* Which unpublished changes a failed save has to throw away to make progress.
|
|
5447
|
+
*
|
|
5448
|
+
* `/save` used to refuse the whole commit when any one patch could not be
|
|
5449
|
+
* applied: nothing was written, for any module, and the change stayed on disk to
|
|
5450
|
+
* fail again on the next attempt. With auto-save that is not a refusal, it is a
|
|
5451
|
+
* dead stop — the editor keeps typing, every save fails on the same patch, and
|
|
5452
|
+
* nothing is ever written again.
|
|
5453
|
+
*
|
|
5454
|
+
* So in `fs` mode the failing change is removed instead, and the person editing
|
|
5455
|
+
* is told. The rule for what goes with it:
|
|
5456
|
+
*
|
|
5457
|
+
* **From the first failure onward, within that module.** A patch is a set of ops
|
|
5458
|
+
* against the source as the patches before it left it. Once one has not been
|
|
5459
|
+
* applied, every later patch in that module was computed against a state that no
|
|
5460
|
+
* longer exists, so keeping them would be keeping changes whose meaning has
|
|
5461
|
+
* already been lost. That is `triedPatches` (the one that failed) plus
|
|
5462
|
+
* `skippedPatches` (the tail that was never attempted).
|
|
5463
|
+
*
|
|
5464
|
+
* **Other modules are untouched.** `prepare` walks each module's chain
|
|
5465
|
+
* independently, so a broken chain in one file says nothing about another.
|
|
5466
|
+
*
|
|
5467
|
+
* **And a module that failed as a WHOLE keeps everything.** When the `.val.ts`
|
|
5468
|
+
* cannot be read at all, `prepare` reports every one of its patches as skipped
|
|
5469
|
+
* and names none of them as unappliable — the file is what failed, not the
|
|
5470
|
+
* changes. Deleting them there would be destroying good edits because a file was
|
|
5471
|
+
* missing for a moment: rename a module, or switch branch, with unsaved changes
|
|
5472
|
+
* to it and the next auto-save would silently take them all. Skipped is only
|
|
5473
|
+
* collateral when there is something in that module for it to be collateral OF.
|
|
5474
|
+
*/
|
|
5475
|
+
|
|
5476
|
+
/**
|
|
5477
|
+
* The message for a patch that did not itself fail but sits behind one that did.
|
|
5478
|
+
*
|
|
5479
|
+
* Worth distinguishing: "yours was broken" and "something before yours was
|
|
5480
|
+
* broken" are different things to be told, and only the first is a bug in the
|
|
5481
|
+
* change itself.
|
|
5482
|
+
*/
|
|
5483
|
+
function collateralMessage(moduleFilePath) {
|
|
5484
|
+
return `An earlier change to ${moduleFilePath} could not be applied, so this one no longer fits the content it was written against.`;
|
|
5485
|
+
}
|
|
5486
|
+
function computePatchesToDrop(prepared) {
|
|
5487
|
+
const dropped = [];
|
|
5488
|
+
const seen = new Set();
|
|
5489
|
+
const add = (moduleFilePath, patchId) => {
|
|
5490
|
+
var _prepared$unappliable;
|
|
5491
|
+
if (seen.has(patchId)) {
|
|
5492
|
+
return;
|
|
5493
|
+
}
|
|
5494
|
+
seen.add(patchId);
|
|
5495
|
+
dropped.push({
|
|
5496
|
+
patchId,
|
|
5497
|
+
moduleFilePath,
|
|
5498
|
+
message: ((_prepared$unappliable = prepared.unappliablePatches[patchId]) === null || _prepared$unappliable === void 0 ? void 0 : _prepared$unappliable.message) ?? collateralMessage(moduleFilePath)
|
|
5499
|
+
});
|
|
5500
|
+
};
|
|
5501
|
+
|
|
5502
|
+
// Tried before skipped, so the change that actually failed is named first —
|
|
5503
|
+
// it is the one worth reading.
|
|
5504
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5505
|
+
for (const patchId of patchIds) {
|
|
5506
|
+
add(moduleFilePath, patchId);
|
|
5507
|
+
}
|
|
5508
|
+
}
|
|
5509
|
+
/*
|
|
5510
|
+
* Which modules actually had a change fail, as opposed to being unreadable.
|
|
5511
|
+
*
|
|
5512
|
+
* `triedPatches` holds only failures, and `unappliablePatches` names the
|
|
5513
|
+
* failure itself, so between them they are the whole answer. A module in
|
|
5514
|
+
* neither has nothing to blame its skipped tail on.
|
|
5515
|
+
*/
|
|
5516
|
+
const failedModules = new Set();
|
|
5517
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5518
|
+
if (patchIds.length > 0) {
|
|
5519
|
+
failedModules.add(moduleFilePath);
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
for (const failure of Object.values(prepared.unappliablePatches)) {
|
|
5523
|
+
failedModules.add(failure.moduleFilePath);
|
|
5524
|
+
}
|
|
5525
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.skippedPatches)) {
|
|
5526
|
+
if (!failedModules.has(moduleFilePath)) {
|
|
5527
|
+
// The module failed as a whole. Its changes are fine and are kept, so the
|
|
5528
|
+
// save refuses instead — which is what it did before auto-save, and is
|
|
5529
|
+
// right here: this one resolves itself when the file comes back.
|
|
5530
|
+
continue;
|
|
5531
|
+
}
|
|
5532
|
+
for (const patchId of patchIds) {
|
|
5533
|
+
add(moduleFilePath, patchId);
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
|
|
5537
|
+
/*
|
|
5538
|
+
* A patch reported unappliable that neither list holds.
|
|
5539
|
+
*
|
|
5540
|
+
* Belt and braces: the two lists are how `prepare` reports the shape of the
|
|
5541
|
+
* failure, and this is how it reports the failure itself. If they ever
|
|
5542
|
+
* disagree, leaving a patch behind means the next save fails on it again —
|
|
5543
|
+
* the exact dead stop this function exists to prevent.
|
|
5544
|
+
*/
|
|
5545
|
+
for (const [patchId, failure] of Object.entries(prepared.unappliablePatches)) {
|
|
5546
|
+
add(failure.moduleFilePath, patchId);
|
|
5547
|
+
}
|
|
5548
|
+
return dropped;
|
|
5549
|
+
}
|
|
5550
|
+
|
|
5445
5551
|
const textEncoder$2 = new TextEncoder();
|
|
5446
5552
|
const PatchId = z.z.string().refine(s => !!s); // TODO: validate
|
|
5447
5553
|
const CommitSha = z.z.string().refine(s => !!s); // TODO: validate
|
|
@@ -8139,11 +8245,65 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8139
8245
|
patchIds,
|
|
8140
8246
|
excludePatchOps: false
|
|
8141
8247
|
});
|
|
8248
|
+
/*
|
|
8249
|
+
* Exactly the patches this request consumes, and the ONLY ones it may
|
|
8250
|
+
* delete afterwards.
|
|
8251
|
+
*
|
|
8252
|
+
* Taken from what was fetched rather than from what was asked for, and
|
|
8253
|
+
* held across everything below, because the store keeps accepting writes
|
|
8254
|
+
* while this runs. A change typed during a save is not in this list, is
|
|
8255
|
+
* not applied, and must still be here when the save finishes — that is
|
|
8256
|
+
* the whole contract, and `deleteAllPatches` broke it by removing the
|
|
8257
|
+
* store wholesale.
|
|
8258
|
+
*/
|
|
8259
|
+
const consumed = patches.patches.map(patch => patch.patchId);
|
|
8142
8260
|
const analysis = serverOps.analyzePatches(patches.patches, patches.commits, commit);
|
|
8143
|
-
|
|
8261
|
+
let preparedCommit = await serverOps.prepare({
|
|
8144
8262
|
...analysis,
|
|
8145
8263
|
...patches
|
|
8146
8264
|
});
|
|
8265
|
+
/*
|
|
8266
|
+
* In `fs` mode a change that cannot be applied is removed rather than
|
|
8267
|
+
* blocking the save.
|
|
8268
|
+
*
|
|
8269
|
+
* Refusing the whole commit is right for a git commit — it is one
|
|
8270
|
+
* atomic thing, and the content api owns its own patches. It is wrong
|
|
8271
|
+
* for auto-save: the editor keeps typing, every save fails on the same
|
|
8272
|
+
* patch, and nothing is ever written again. So the failing change and
|
|
8273
|
+
* the rest of its module's chain go, the rest is written, and the person
|
|
8274
|
+
* editing is told what was thrown away.
|
|
8275
|
+
*/
|
|
8276
|
+
let removed = [];
|
|
8277
|
+
if (preparedCommit.hasErrors && serverOps instanceof ValOpsFS) {
|
|
8278
|
+
removed = computePatchesToDrop(preparedCommit);
|
|
8279
|
+
/*
|
|
8280
|
+
* Nothing to drop means nothing a second `prepare` could do better.
|
|
8281
|
+
*
|
|
8282
|
+
* `hasErrors` covers failures no patch is to blame for — a binary file
|
|
8283
|
+
* that could not be written, a module that could not be formatted, a
|
|
8284
|
+
* `.val.ts` that could not be read at all. Re-preparing every module
|
|
8285
|
+
* with an unchanged patch set would fail identically, and logging
|
|
8286
|
+
* "removed unpublished changes []" on the way would say something
|
|
8287
|
+
* untrue. It falls through to the 400 instead, which is the honest
|
|
8288
|
+
* answer: this save did not happen and the changes are still here.
|
|
8289
|
+
*/
|
|
8290
|
+
if (removed.length > 0) {
|
|
8291
|
+
const doomed = new Set(removed.map(entry => entry.patchId));
|
|
8292
|
+
const survivors = patches.patches.filter(patch => !doomed.has(patch.patchId));
|
|
8293
|
+
const survivingPatches = {
|
|
8294
|
+
...patches,
|
|
8295
|
+
patches: survivors
|
|
8296
|
+
};
|
|
8297
|
+
// One retry is enough: what is left of each module is the prefix
|
|
8298
|
+
// that already applied, and `prepare` walks modules independently,
|
|
8299
|
+
// so nothing new can fail.
|
|
8300
|
+
preparedCommit = await serverOps.prepare({
|
|
8301
|
+
...serverOps.analyzePatches(survivors, patches.commits, commit),
|
|
8302
|
+
...survivingPatches
|
|
8303
|
+
});
|
|
8304
|
+
console.error("Val: removed unpublished changes that could not be applied", removed);
|
|
8305
|
+
}
|
|
8306
|
+
}
|
|
8147
8307
|
if (preparedCommit.hasErrors) {
|
|
8148
8308
|
console.error("Failed to create commit", JSON.stringify({
|
|
8149
8309
|
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
|
@@ -8196,13 +8356,31 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8196
8356
|
}
|
|
8197
8357
|
};
|
|
8198
8358
|
}
|
|
8199
|
-
|
|
8200
|
-
|
|
8359
|
+
/*
|
|
8360
|
+
* Only what this request consumed.
|
|
8361
|
+
*
|
|
8362
|
+
* This used to be `deleteAllPatches()`, which renamed the whole store
|
|
8363
|
+
* aside and deleted it. Anything typed while the save was in flight
|
|
8364
|
+
* went with it — never applied, and indistinguishable on the client
|
|
8365
|
+
* from having been published, because both look like "the patch is
|
|
8366
|
+
* gone and the base moved". A visible edit that exists nowhere on
|
|
8367
|
+
* disk, with nothing reporting a problem.
|
|
8368
|
+
*/
|
|
8369
|
+
const deleteRes = await serverOps.deletePatches(consumed);
|
|
8370
|
+
if ("error" in deleteRes && deleteRes.error !== undefined) {
|
|
8201
8371
|
console.error(`Val got an error while cleaning up patches after publish: ${deleteRes.error.message}`);
|
|
8372
|
+
} else if ("errors" in deleteRes && deleteRes.errors !== undefined) {
|
|
8373
|
+
// Per patch, and not fatal: the files are written, so the save
|
|
8374
|
+
// succeeded. A patch left behind is announced again on the next
|
|
8375
|
+
// stat, applies to a base that already contains it, and is removed
|
|
8376
|
+
// then.
|
|
8377
|
+
console.error("Val: could not clean up some patches after publish", deleteRes.errors);
|
|
8202
8378
|
}
|
|
8203
8379
|
return {
|
|
8204
8380
|
status: 200,
|
|
8205
|
-
json:
|
|
8381
|
+
json: removed.length > 0 ? {
|
|
8382
|
+
removed
|
|
8383
|
+
} : {}
|
|
8206
8384
|
};
|
|
8207
8385
|
} else if (serverOps instanceof ValOpsHttp) {
|
|
8208
8386
|
if (auth.error === undefined && auth.id) {
|
|
@@ -5408,6 +5408,112 @@ class FSOpsHost {
|
|
|
5408
5408
|
}
|
|
5409
5409
|
}
|
|
5410
5410
|
|
|
5411
|
+
/**
|
|
5412
|
+
* Which unpublished changes a failed save has to throw away to make progress.
|
|
5413
|
+
*
|
|
5414
|
+
* `/save` used to refuse the whole commit when any one patch could not be
|
|
5415
|
+
* applied: nothing was written, for any module, and the change stayed on disk to
|
|
5416
|
+
* fail again on the next attempt. With auto-save that is not a refusal, it is a
|
|
5417
|
+
* dead stop — the editor keeps typing, every save fails on the same patch, and
|
|
5418
|
+
* nothing is ever written again.
|
|
5419
|
+
*
|
|
5420
|
+
* So in `fs` mode the failing change is removed instead, and the person editing
|
|
5421
|
+
* is told. The rule for what goes with it:
|
|
5422
|
+
*
|
|
5423
|
+
* **From the first failure onward, within that module.** A patch is a set of ops
|
|
5424
|
+
* against the source as the patches before it left it. Once one has not been
|
|
5425
|
+
* applied, every later patch in that module was computed against a state that no
|
|
5426
|
+
* longer exists, so keeping them would be keeping changes whose meaning has
|
|
5427
|
+
* already been lost. That is `triedPatches` (the one that failed) plus
|
|
5428
|
+
* `skippedPatches` (the tail that was never attempted).
|
|
5429
|
+
*
|
|
5430
|
+
* **Other modules are untouched.** `prepare` walks each module's chain
|
|
5431
|
+
* independently, so a broken chain in one file says nothing about another.
|
|
5432
|
+
*
|
|
5433
|
+
* **And a module that failed as a WHOLE keeps everything.** When the `.val.ts`
|
|
5434
|
+
* cannot be read at all, `prepare` reports every one of its patches as skipped
|
|
5435
|
+
* and names none of them as unappliable — the file is what failed, not the
|
|
5436
|
+
* changes. Deleting them there would be destroying good edits because a file was
|
|
5437
|
+
* missing for a moment: rename a module, or switch branch, with unsaved changes
|
|
5438
|
+
* to it and the next auto-save would silently take them all. Skipped is only
|
|
5439
|
+
* collateral when there is something in that module for it to be collateral OF.
|
|
5440
|
+
*/
|
|
5441
|
+
|
|
5442
|
+
/**
|
|
5443
|
+
* The message for a patch that did not itself fail but sits behind one that did.
|
|
5444
|
+
*
|
|
5445
|
+
* Worth distinguishing: "yours was broken" and "something before yours was
|
|
5446
|
+
* broken" are different things to be told, and only the first is a bug in the
|
|
5447
|
+
* change itself.
|
|
5448
|
+
*/
|
|
5449
|
+
function collateralMessage(moduleFilePath) {
|
|
5450
|
+
return `An earlier change to ${moduleFilePath} could not be applied, so this one no longer fits the content it was written against.`;
|
|
5451
|
+
}
|
|
5452
|
+
function computePatchesToDrop(prepared) {
|
|
5453
|
+
const dropped = [];
|
|
5454
|
+
const seen = new Set();
|
|
5455
|
+
const add = (moduleFilePath, patchId) => {
|
|
5456
|
+
var _prepared$unappliable;
|
|
5457
|
+
if (seen.has(patchId)) {
|
|
5458
|
+
return;
|
|
5459
|
+
}
|
|
5460
|
+
seen.add(patchId);
|
|
5461
|
+
dropped.push({
|
|
5462
|
+
patchId,
|
|
5463
|
+
moduleFilePath,
|
|
5464
|
+
message: ((_prepared$unappliable = prepared.unappliablePatches[patchId]) === null || _prepared$unappliable === void 0 ? void 0 : _prepared$unappliable.message) ?? collateralMessage(moduleFilePath)
|
|
5465
|
+
});
|
|
5466
|
+
};
|
|
5467
|
+
|
|
5468
|
+
// Tried before skipped, so the change that actually failed is named first —
|
|
5469
|
+
// it is the one worth reading.
|
|
5470
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5471
|
+
for (const patchId of patchIds) {
|
|
5472
|
+
add(moduleFilePath, patchId);
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
/*
|
|
5476
|
+
* Which modules actually had a change fail, as opposed to being unreadable.
|
|
5477
|
+
*
|
|
5478
|
+
* `triedPatches` holds only failures, and `unappliablePatches` names the
|
|
5479
|
+
* failure itself, so between them they are the whole answer. A module in
|
|
5480
|
+
* neither has nothing to blame its skipped tail on.
|
|
5481
|
+
*/
|
|
5482
|
+
const failedModules = new Set();
|
|
5483
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.triedPatches)) {
|
|
5484
|
+
if (patchIds.length > 0) {
|
|
5485
|
+
failedModules.add(moduleFilePath);
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
for (const failure of Object.values(prepared.unappliablePatches)) {
|
|
5489
|
+
failedModules.add(failure.moduleFilePath);
|
|
5490
|
+
}
|
|
5491
|
+
for (const [moduleFilePath, patchIds] of Object.entries(prepared.skippedPatches)) {
|
|
5492
|
+
if (!failedModules.has(moduleFilePath)) {
|
|
5493
|
+
// The module failed as a whole. Its changes are fine and are kept, so the
|
|
5494
|
+
// save refuses instead — which is what it did before auto-save, and is
|
|
5495
|
+
// right here: this one resolves itself when the file comes back.
|
|
5496
|
+
continue;
|
|
5497
|
+
}
|
|
5498
|
+
for (const patchId of patchIds) {
|
|
5499
|
+
add(moduleFilePath, patchId);
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
/*
|
|
5504
|
+
* A patch reported unappliable that neither list holds.
|
|
5505
|
+
*
|
|
5506
|
+
* Belt and braces: the two lists are how `prepare` reports the shape of the
|
|
5507
|
+
* failure, and this is how it reports the failure itself. If they ever
|
|
5508
|
+
* disagree, leaving a patch behind means the next save fails on it again —
|
|
5509
|
+
* the exact dead stop this function exists to prevent.
|
|
5510
|
+
*/
|
|
5511
|
+
for (const [patchId, failure] of Object.entries(prepared.unappliablePatches)) {
|
|
5512
|
+
add(failure.moduleFilePath, patchId);
|
|
5513
|
+
}
|
|
5514
|
+
return dropped;
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5411
5517
|
const textEncoder$2 = new TextEncoder();
|
|
5412
5518
|
const PatchId = z.string().refine(s => !!s); // TODO: validate
|
|
5413
5519
|
const CommitSha = z.string().refine(s => !!s); // TODO: validate
|
|
@@ -8105,11 +8211,65 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8105
8211
|
patchIds,
|
|
8106
8212
|
excludePatchOps: false
|
|
8107
8213
|
});
|
|
8214
|
+
/*
|
|
8215
|
+
* Exactly the patches this request consumes, and the ONLY ones it may
|
|
8216
|
+
* delete afterwards.
|
|
8217
|
+
*
|
|
8218
|
+
* Taken from what was fetched rather than from what was asked for, and
|
|
8219
|
+
* held across everything below, because the store keeps accepting writes
|
|
8220
|
+
* while this runs. A change typed during a save is not in this list, is
|
|
8221
|
+
* not applied, and must still be here when the save finishes — that is
|
|
8222
|
+
* the whole contract, and `deleteAllPatches` broke it by removing the
|
|
8223
|
+
* store wholesale.
|
|
8224
|
+
*/
|
|
8225
|
+
const consumed = patches.patches.map(patch => patch.patchId);
|
|
8108
8226
|
const analysis = serverOps.analyzePatches(patches.patches, patches.commits, commit);
|
|
8109
|
-
|
|
8227
|
+
let preparedCommit = await serverOps.prepare({
|
|
8110
8228
|
...analysis,
|
|
8111
8229
|
...patches
|
|
8112
8230
|
});
|
|
8231
|
+
/*
|
|
8232
|
+
* In `fs` mode a change that cannot be applied is removed rather than
|
|
8233
|
+
* blocking the save.
|
|
8234
|
+
*
|
|
8235
|
+
* Refusing the whole commit is right for a git commit — it is one
|
|
8236
|
+
* atomic thing, and the content api owns its own patches. It is wrong
|
|
8237
|
+
* for auto-save: the editor keeps typing, every save fails on the same
|
|
8238
|
+
* patch, and nothing is ever written again. So the failing change and
|
|
8239
|
+
* the rest of its module's chain go, the rest is written, and the person
|
|
8240
|
+
* editing is told what was thrown away.
|
|
8241
|
+
*/
|
|
8242
|
+
let removed = [];
|
|
8243
|
+
if (preparedCommit.hasErrors && serverOps instanceof ValOpsFS) {
|
|
8244
|
+
removed = computePatchesToDrop(preparedCommit);
|
|
8245
|
+
/*
|
|
8246
|
+
* Nothing to drop means nothing a second `prepare` could do better.
|
|
8247
|
+
*
|
|
8248
|
+
* `hasErrors` covers failures no patch is to blame for — a binary file
|
|
8249
|
+
* that could not be written, a module that could not be formatted, a
|
|
8250
|
+
* `.val.ts` that could not be read at all. Re-preparing every module
|
|
8251
|
+
* with an unchanged patch set would fail identically, and logging
|
|
8252
|
+
* "removed unpublished changes []" on the way would say something
|
|
8253
|
+
* untrue. It falls through to the 400 instead, which is the honest
|
|
8254
|
+
* answer: this save did not happen and the changes are still here.
|
|
8255
|
+
*/
|
|
8256
|
+
if (removed.length > 0) {
|
|
8257
|
+
const doomed = new Set(removed.map(entry => entry.patchId));
|
|
8258
|
+
const survivors = patches.patches.filter(patch => !doomed.has(patch.patchId));
|
|
8259
|
+
const survivingPatches = {
|
|
8260
|
+
...patches,
|
|
8261
|
+
patches: survivors
|
|
8262
|
+
};
|
|
8263
|
+
// One retry is enough: what is left of each module is the prefix
|
|
8264
|
+
// that already applied, and `prepare` walks modules independently,
|
|
8265
|
+
// so nothing new can fail.
|
|
8266
|
+
preparedCommit = await serverOps.prepare({
|
|
8267
|
+
...serverOps.analyzePatches(survivors, patches.commits, commit),
|
|
8268
|
+
...survivingPatches
|
|
8269
|
+
});
|
|
8270
|
+
console.error("Val: removed unpublished changes that could not be applied", removed);
|
|
8271
|
+
}
|
|
8272
|
+
}
|
|
8113
8273
|
if (preparedCommit.hasErrors) {
|
|
8114
8274
|
console.error("Failed to create commit", JSON.stringify({
|
|
8115
8275
|
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
|
@@ -8162,13 +8322,31 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8162
8322
|
}
|
|
8163
8323
|
};
|
|
8164
8324
|
}
|
|
8165
|
-
|
|
8166
|
-
|
|
8325
|
+
/*
|
|
8326
|
+
* Only what this request consumed.
|
|
8327
|
+
*
|
|
8328
|
+
* This used to be `deleteAllPatches()`, which renamed the whole store
|
|
8329
|
+
* aside and deleted it. Anything typed while the save was in flight
|
|
8330
|
+
* went with it — never applied, and indistinguishable on the client
|
|
8331
|
+
* from having been published, because both look like "the patch is
|
|
8332
|
+
* gone and the base moved". A visible edit that exists nowhere on
|
|
8333
|
+
* disk, with nothing reporting a problem.
|
|
8334
|
+
*/
|
|
8335
|
+
const deleteRes = await serverOps.deletePatches(consumed);
|
|
8336
|
+
if ("error" in deleteRes && deleteRes.error !== undefined) {
|
|
8167
8337
|
console.error(`Val got an error while cleaning up patches after publish: ${deleteRes.error.message}`);
|
|
8338
|
+
} else if ("errors" in deleteRes && deleteRes.errors !== undefined) {
|
|
8339
|
+
// Per patch, and not fatal: the files are written, so the save
|
|
8340
|
+
// succeeded. A patch left behind is announced again on the next
|
|
8341
|
+
// stat, applies to a base that already contains it, and is removed
|
|
8342
|
+
// then.
|
|
8343
|
+
console.error("Val: could not clean up some patches after publish", deleteRes.errors);
|
|
8168
8344
|
}
|
|
8169
8345
|
return {
|
|
8170
8346
|
status: 200,
|
|
8171
|
-
json:
|
|
8347
|
+
json: removed.length > 0 ? {
|
|
8348
|
+
removed
|
|
8349
|
+
} : {}
|
|
8172
8350
|
};
|
|
8173
8351
|
} else if (serverOps instanceof ValOpsHttp) {
|
|
8174
8352
|
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.103.
|
|
19
|
+
"version": "0.103.2",
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@prettier/sync": "^0.6.1",
|
|
22
22
|
"@types/jest": "^30.0.0"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"zod-validation-error": "^5.0.0",
|
|
32
32
|
"@valbuild/core": "0.103.0",
|
|
33
33
|
"@valbuild/shared": "0.103.0",
|
|
34
|
-
"@valbuild/ui": "0.103.
|
|
34
|
+
"@valbuild/ui": "0.103.2"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=18.17.0"
|