@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.
@@ -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;
@@ -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 render from its schema INSTANCE.
201
+ * Reifies each module's previews from its schema INSTANCE.
202
202
  *
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
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 renders at all. See
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
- getRenders(schemas: Schemas, sources: Sources): Promise<{
210
- renders: Record<ModuleFilePath, ReifiedRender | null>;
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): 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>>;
@@ -2045,24 +2045,27 @@ class ValOps {
2045
2045
  };
2046
2046
  }
2047
2047
 
2048
- // #region getRenders
2048
+ // #region getPreviews
2049
2049
  /**
2050
- * Reifies each module's render from its schema INSTANCE.
2050
+ * Reifies each module's previews from its schema INSTANCE.
2051
2051
  *
2052
- * Kept even though the Studio also computes renders client-side: `select` is a
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 renders at all. See
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 getRenders(schemas, sources) {
2059
- const renders = {};
2061
+ async getPreviews(schemas, sources) {
2062
+ const previews = {};
2060
2063
  for (const [pathS, schema] of Object.entries(schemas)) {
2061
2064
  const path = pathS;
2062
- renders[path] = schema["executeRender"](path, sources[path]);
2065
+ previews[path] = schema["executePreview"](path, sources[path]);
2063
2066
  }
2064
2067
  return {
2065
- renders
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: renders are computed here even when the client applies patches
7874
- // itself: the render select functions live on the Schema instances and
7875
- // are not part of the serialized schema, so the client cannot derive
7876
- // them. They are computed on the patched sources, so that previews
7877
- // (list titles / subtitles / images) reflect the patches that apply.
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 renderRes = await serverOps.getRenders(schemasRes, patchedSources);
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
- render: renderRes.renders[moduleFilePath] || null,
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
- const preparedCommit = await serverOps.prepare({
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
- const deleteRes = await serverOps.deleteAllPatches();
8200
- if (deleteRes.error) {
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: {} // TODO:
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 getRenders
2048
+ // #region getPreviews
2049
2049
  /**
2050
- * Reifies each module's render from its schema INSTANCE.
2050
+ * Reifies each module's previews from its schema INSTANCE.
2051
2051
  *
2052
- * Kept even though the Studio also computes renders client-side: `select` is a
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 renders at all. See
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 getRenders(schemas, sources) {
2059
- const renders = {};
2061
+ async getPreviews(schemas, sources) {
2062
+ const previews = {};
2060
2063
  for (const [pathS, schema] of Object.entries(schemas)) {
2061
2064
  const path = pathS;
2062
- renders[path] = schema["executeRender"](path, sources[path]);
2065
+ previews[path] = schema["executePreview"](path, sources[path]);
2063
2066
  }
2064
2067
  return {
2065
- renders
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: renders are computed here even when the client applies patches
7874
- // itself: the render select functions live on the Schema instances and
7875
- // are not part of the serialized schema, so the client cannot derive
7876
- // them. They are computed on the patched sources, so that previews
7877
- // (list titles / subtitles / images) reflect the patches that apply.
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 renderRes = await serverOps.getRenders(schemasRes, patchedSources);
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
- render: renderRes.renders[moduleFilePath] || null,
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
- const preparedCommit = await serverOps.prepare({
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
- const deleteRes = await serverOps.deleteAllPatches();
8200
- if (deleteRes.error) {
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: {} // TODO:
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 getRenders
2014
+ // #region getPreviews
2015
2015
  /**
2016
- * Reifies each module's render from its schema INSTANCE.
2016
+ * Reifies each module's previews from its schema INSTANCE.
2017
2017
  *
2018
- * Kept even though the Studio also computes renders client-side: `select` is a
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 renders at all. See
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 getRenders(schemas, sources) {
2025
- const renders = {};
2027
+ async getPreviews(schemas, sources) {
2028
+ const previews = {};
2026
2029
  for (const [pathS, schema] of Object.entries(schemas)) {
2027
2030
  const path = pathS;
2028
- renders[path] = schema["executeRender"](path, sources[path]);
2031
+ previews[path] = schema["executePreview"](path, sources[path]);
2029
2032
  }
2030
2033
  return {
2031
- renders
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: renders are computed here even when the client applies patches
7840
- // itself: the render select functions live on the Schema instances and
7841
- // are not part of the serialized schema, so the client cannot derive
7842
- // them. They are computed on the patched sources, so that previews
7843
- // (list titles / subtitles / images) reflect the patches that apply.
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 renderRes = await serverOps.getRenders(schemasRes, patchedSources);
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
- render: renderRes.renders[moduleFilePath] || null,
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
- const preparedCommit = await serverOps.prepare({
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
- const deleteRes = await serverOps.deleteAllPatches();
8166
- if (deleteRes.error) {
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: {} // TODO:
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.103.1",
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.103.0",
33
- "@valbuild/shared": "0.103.0",
34
- "@valbuild/ui": "0.103.1"
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"