@valbuild/server 0.105.0 → 0.105.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/declarations/src/ValOps.d.ts +112 -0
- package/dist/declarations/src/ValOpsFS.d.ts +9 -0
- package/dist/declarations/src/patchStore.d.ts +45 -0
- package/dist/valbuild-server.cjs.dev.js +554 -22
- package/dist/valbuild-server.cjs.prod.js +554 -22
- package/dist/valbuild-server.esm.js +555 -23
- package/package.json +4 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { pipe, result, array } from '@valbuild/core/fp';
|
|
3
3
|
import { PatchError, deepEqual, parseAndValidateArrayIndex, isNotRoot, applyPatch, JSONOps, deepClone, sourceToPatchPath } from '@valbuild/core/patch';
|
|
4
|
-
import { derefPatch, RecordSchema, Internal, extractValModules, VAL_EXTENSION, ImageSchema, DEFAULT_CONTENT_HOST, hasRemoteFileSchema } from '@valbuild/core';
|
|
4
|
+
import { derefPatch, RecordSchema, Internal, extractValModules, computeValModuleShas, VAL_EXTENSION, ImageSchema, DEFAULT_CONTENT_HOST, hasRemoteFileSchema } from '@valbuild/core';
|
|
5
5
|
export { hasRemoteFileSchema } from '@valbuild/core';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import path__default from 'path';
|
|
@@ -1656,6 +1656,40 @@ class ValOps {
|
|
|
1656
1656
|
|
|
1657
1657
|
/** The sha256 / hash of schema + config - if this changes users needs to reload */
|
|
1658
1658
|
|
|
1659
|
+
/**
|
|
1660
|
+
* What the SHAs above are a fold over, so they can be recomputed.
|
|
1661
|
+
*
|
|
1662
|
+
* See {@link promoteCommittedSources}: the one thing that changes sources
|
|
1663
|
+
* without re-evaluating the modules is a save, and it has to be able to move
|
|
1664
|
+
* the SHAs with them.
|
|
1665
|
+
*/
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* The extraction's OWN module errors, which are what the fold was given.
|
|
1669
|
+
*
|
|
1670
|
+
* Not the same list as {@link modulesErrors}: that one has the nested
|
|
1671
|
+
* `.jsonValues()` errors concatenated on, and those were never part of the
|
|
1672
|
+
* hash. Re-folding with the wrong list changes the base SHA for no reason.
|
|
1673
|
+
*/
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* What a save has told us each `.jsonValues()` entry now holds.
|
|
1677
|
+
*
|
|
1678
|
+
* The entry twin of {@link sources}, and it has to be separate because an
|
|
1679
|
+
* entry's content is not IN the source: the source holds a marker, and
|
|
1680
|
+
* {@link getJsonEntries} resolves it by awaiting the marker's own `import()`.
|
|
1681
|
+
* That resolves from the module registry, so after `/save` rewrites a
|
|
1682
|
+
* `*.val.json` the thunk keeps answering with the content from before — and
|
|
1683
|
+
* unlike a module source there is nothing to re-extract, because the memo was
|
|
1684
|
+
* never holding the content in the first place.
|
|
1685
|
+
*
|
|
1686
|
+
* `null` for an entry the commit deleted.
|
|
1687
|
+
*
|
|
1688
|
+
* Never cleared: it describes what is on disk. A host rebuild makes a new
|
|
1689
|
+
* instance, which is the right reset. Bounded by the project's entry count,
|
|
1690
|
+
* holding only the latest content per key.
|
|
1691
|
+
*/
|
|
1692
|
+
adoptedJsonEntries = new Map();
|
|
1659
1693
|
constructor(valModules, options) {
|
|
1660
1694
|
this.valModules = valModules;
|
|
1661
1695
|
this.options = options;
|
|
@@ -1666,6 +1700,8 @@ class ValOps {
|
|
|
1666
1700
|
this.sourcesSha = null;
|
|
1667
1701
|
this.configSha = null;
|
|
1668
1702
|
this.modulesErrors = null;
|
|
1703
|
+
this.shaEntries = null;
|
|
1704
|
+
this.shaModuleErrors = null;
|
|
1669
1705
|
}
|
|
1670
1706
|
|
|
1671
1707
|
// #region stat
|
|
@@ -1691,6 +1727,8 @@ class ValOps {
|
|
|
1691
1727
|
this.sourcesSha = extracted.sourcesSha;
|
|
1692
1728
|
this.configSha = extracted.configSha;
|
|
1693
1729
|
this.modulesErrors = moduleErrors;
|
|
1730
|
+
this.shaEntries = extracted.shaEntries;
|
|
1731
|
+
this.shaModuleErrors = extracted.moduleErrors;
|
|
1694
1732
|
return {
|
|
1695
1733
|
baseSha: this.baseSha,
|
|
1696
1734
|
schemaSha: this.schemaSha,
|
|
@@ -1711,6 +1749,141 @@ class ValOps {
|
|
|
1711
1749
|
moduleErrors: this.modulesErrors
|
|
1712
1750
|
};
|
|
1713
1751
|
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* These patches are on disk now: adopt what they produced as the committed
|
|
1755
|
+
* sources.
|
|
1756
|
+
*
|
|
1757
|
+
* The entry point for the mechanism {@link promoteCommittedSources} describes,
|
|
1758
|
+
* and the only one — a caller hands over the analysis it just committed and
|
|
1759
|
+
* this works out the rest, so the rule about which sources are adopted lives
|
|
1760
|
+
* in one place rather than at each save site.
|
|
1761
|
+
*
|
|
1762
|
+
* A module whose patches could not be applied cleanly is left alone. `/save`
|
|
1763
|
+
* refuses the whole commit before reaching here if `prepare` found errors, so
|
|
1764
|
+
* this cannot normally fire — but adopting a partially patched source would
|
|
1765
|
+
* put content in the memo that is not what was written, which is worse than
|
|
1766
|
+
* being stale.
|
|
1767
|
+
*/
|
|
1768
|
+
async adoptCommittedSources(analysis, preparedCommit) {
|
|
1769
|
+
// Read BEFORE anything is promoted: this applies the chain to the sources as
|
|
1770
|
+
// they stand, and promoting first would apply the same patches twice.
|
|
1771
|
+
const {
|
|
1772
|
+
sources,
|
|
1773
|
+
errors
|
|
1774
|
+
} = await this.getSources(analysis);
|
|
1775
|
+
const adopt = {};
|
|
1776
|
+
for (const [moduleFilePathS, source] of Object.entries(sources)) {
|
|
1777
|
+
const moduleFilePath = moduleFilePathS;
|
|
1778
|
+
if (errors[moduleFilePath] !== undefined) {
|
|
1779
|
+
console.error("Val: not adopting the committed source of a module whose patches " + "did not apply cleanly. Its content here stays as it was until the " + "modules are re-evaluated.", {
|
|
1780
|
+
moduleFilePath,
|
|
1781
|
+
errors: errors[moduleFilePath]
|
|
1782
|
+
});
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
adopt[moduleFilePath] = source;
|
|
1786
|
+
}
|
|
1787
|
+
this.promoteCommittedSources(adopt);
|
|
1788
|
+
/**
|
|
1789
|
+
* And the `.jsonValues()` entry content, which the sources above do not
|
|
1790
|
+
* carry — they hold markers. See {@link adoptedJsonEntries}.
|
|
1791
|
+
*
|
|
1792
|
+
* Only for a module whose source was adopted. The source is what frames an
|
|
1793
|
+
* entry: it decides which keys exist at all, so adopting one without the
|
|
1794
|
+
* other would leave the content and the key set describing different
|
|
1795
|
+
* moments.
|
|
1796
|
+
*/
|
|
1797
|
+
for (const [moduleFilePathS, entries] of Object.entries(preparedCommit.patchedJsonEntries)) {
|
|
1798
|
+
const moduleFilePath = moduleFilePathS;
|
|
1799
|
+
if (adopt[moduleFilePath] === undefined) {
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
const adopted = this.adoptedJsonEntries.get(moduleFilePath) ?? new Map();
|
|
1803
|
+
for (const [entryKey, content] of Object.entries(entries)) {
|
|
1804
|
+
adopted.set(entryKey, content);
|
|
1805
|
+
}
|
|
1806
|
+
this.adoptedJsonEntries.set(moduleFilePath, adopted);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* Adopt sources that have just been written to disk, and move the SHAs with
|
|
1812
|
+
* them.
|
|
1813
|
+
*
|
|
1814
|
+
* ## Why this exists rather than an invalidation
|
|
1815
|
+
*
|
|
1816
|
+
* The obvious thing — throw the memo away after a save so the next read
|
|
1817
|
+
* re-extracts — does not work, and quietly. `extractValModules` gets a
|
|
1818
|
+
* module's content by awaiting its `def`, which is the app's own `import()`:
|
|
1819
|
+
* that resolves from the MODULE REGISTRY, not from the file on disk. Right
|
|
1820
|
+
* after `/save` rewrites a `.val.ts`, the registry still holds the module as
|
|
1821
|
+
* it was evaluated before, so a re-extraction returns the pre-save content and
|
|
1822
|
+
* stores it as fresh. What actually replaces it is the host rebuilding its
|
|
1823
|
+
* module graph and constructing a new `ValOps` — which happens on its own
|
|
1824
|
+
* schedule, and until it does, every read is stale.
|
|
1825
|
+
*
|
|
1826
|
+
* Stale reads here are not abstract: `getJsonEntry` resolves a
|
|
1827
|
+
* `.jsonValues()` entry from the committed source and then replays pending
|
|
1828
|
+
* patches over it, so once a publish has removed the patches, a page rendering
|
|
1829
|
+
* draft content gets the committed value — the one this memo is holding from
|
|
1830
|
+
* before the publish.
|
|
1831
|
+
*
|
|
1832
|
+
* So the save tells us instead. It has just computed what the new committed
|
|
1833
|
+
* sources are, and that answer does not depend on anything being
|
|
1834
|
+
* re-evaluated.
|
|
1835
|
+
*
|
|
1836
|
+
* ## And the SHAs move
|
|
1837
|
+
*
|
|
1838
|
+
* Deliberately, and this is the part with consequences. `baseSha` and
|
|
1839
|
+
* `sourcesSha` identify the sources being served; leaving them still while the
|
|
1840
|
+
* sources move would put a value other code compares against into
|
|
1841
|
+
* disagreement with what it describes. Moving them means a `fs`-mode base SHA
|
|
1842
|
+
* changes within a server's lifetime for the first time, which is a signal the
|
|
1843
|
+
* studio already knows how to read: `PatchStore.reconcileVanished` uses a
|
|
1844
|
+
* moved base to tell "these patches were published" from "these patches were
|
|
1845
|
+
* discarded", and takes them out of the chain without reverting the fields —
|
|
1846
|
+
* which is what a second tab watching a publish needs and could not get
|
|
1847
|
+
* before.
|
|
1848
|
+
*
|
|
1849
|
+
* A module the fold does not know is ignored rather than appended: the fold's
|
|
1850
|
+
* order is `val.modules`, and a path that is not in it has no position, so
|
|
1851
|
+
* there is no honest answer for where its hash would go. It also cannot happen
|
|
1852
|
+
* — a save only ever writes modules it read from here.
|
|
1853
|
+
*/
|
|
1854
|
+
promoteCommittedSources(patched) {
|
|
1855
|
+
if (this.sources === null || this.shaEntries === null || this.shaModuleErrors === null) {
|
|
1856
|
+
// Nothing has been read yet, so there is no stale answer to correct and
|
|
1857
|
+
// no fold to replay. The first read extracts, as it always would.
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const known = new Set(this.shaEntries.map(entry => entry.path));
|
|
1861
|
+
const adopt = Object.entries(patched).filter(([moduleFilePath, source]) => source !== undefined && known.has(moduleFilePath));
|
|
1862
|
+
if (adopt.length === 0) {
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const bySource = new Map(adopt);
|
|
1866
|
+
// A new object rather than a mutation: `getSources` hands this out, and a
|
|
1867
|
+
// caller holding it must not have the ground move under it.
|
|
1868
|
+
this.sources = {
|
|
1869
|
+
...this.sources
|
|
1870
|
+
};
|
|
1871
|
+
for (const [moduleFilePath, source] of adopt) {
|
|
1872
|
+
this.sources[moduleFilePath] = source;
|
|
1873
|
+
}
|
|
1874
|
+
this.shaEntries = this.shaEntries.map(entry => {
|
|
1875
|
+
const source = bySource.get(entry.path);
|
|
1876
|
+
return source === undefined ? entry : {
|
|
1877
|
+
...entry,
|
|
1878
|
+
source
|
|
1879
|
+
};
|
|
1880
|
+
});
|
|
1881
|
+
const shas = computeValModuleShas(this.valModules.config, this.shaEntries, this.shaModuleErrors);
|
|
1882
|
+
this.baseSha = shas.baseSha;
|
|
1883
|
+
this.schemaSha = shas.schemaSha;
|
|
1884
|
+
this.sourcesSha = shas.sourcesSha;
|
|
1885
|
+
this.configSha = shas.configSha;
|
|
1886
|
+
}
|
|
1714
1887
|
async init() {
|
|
1715
1888
|
const {
|
|
1716
1889
|
baseSha,
|
|
@@ -1860,6 +2033,31 @@ class ValOps {
|
|
|
1860
2033
|
baseContent: undefined
|
|
1861
2034
|
};
|
|
1862
2035
|
}
|
|
2036
|
+
/**
|
|
2037
|
+
* What a save told us this entry holds, ahead of the thunk.
|
|
2038
|
+
*
|
|
2039
|
+
* The thunk resolves from the module registry, so after `/save` rewrites
|
|
2040
|
+
* a `*.val.json` it keeps answering with the content from before — and
|
|
2041
|
+
* there is nothing to re-extract, because the committed content was
|
|
2042
|
+
* never in the memoised source to begin with. See
|
|
2043
|
+
* {@link adoptedJsonEntries}.
|
|
2044
|
+
*
|
|
2045
|
+
* `null` means the commit deleted the entry, which is reported the same
|
|
2046
|
+
* way an absent key is. (Nearly unreachable — a `remove` also drops the
|
|
2047
|
+
* thunk from the `.val.ts`, so the key is gone from `record` once the
|
|
2048
|
+
* source is adopted — but the map says it, so this says it too.)
|
|
2049
|
+
*
|
|
2050
|
+
* The BASELINE only. Pending patches replay over it below exactly as
|
|
2051
|
+
* they do over the thunk's answer.
|
|
2052
|
+
*/
|
|
2053
|
+
const adopted = this.adoptedJsonEntries.get(moduleFilePath);
|
|
2054
|
+
if (adopted !== undefined && adopted.has(entryKey)) {
|
|
2055
|
+
const content = adopted.get(entryKey);
|
|
2056
|
+
return {
|
|
2057
|
+
entryKey,
|
|
2058
|
+
baseContent: content === null ? undefined : content
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
1863
2061
|
const thunk = Internal.getJsonImport(marker);
|
|
1864
2062
|
if (!thunk) {
|
|
1865
2063
|
return {
|
|
@@ -2562,6 +2760,22 @@ class ValOps {
|
|
|
2562
2760
|
|
|
2563
2761
|
// jsonValues entry content, keyed by `*.val.json` path. `null` = delete.
|
|
2564
2762
|
const jsonEntryContents = new Map();
|
|
2763
|
+
/**
|
|
2764
|
+
* The same content, keyed by ENTRY KEY rather than by file path.
|
|
2765
|
+
*
|
|
2766
|
+
* The path is what gets written; the key is what a reader asks for, and it
|
|
2767
|
+
* is dropped at the flush below. Reconstructing it afterwards is not on:
|
|
2768
|
+
* TWO producers turn a key into a path — `resolveEntryJsonPath` and
|
|
2769
|
+
* `getNewJsonEntryPaths`, the latter a locked convention for `add` and a
|
|
2770
|
+
* move's destination — and a marker does not carry its path at read time
|
|
2771
|
+
* (see `jsonEntryFiles.ts`). So it is recorded where the key is known.
|
|
2772
|
+
*
|
|
2773
|
+
* Read by `ValOps.adoptCommittedSources`, so a save can tell this instance
|
|
2774
|
+
* what an entry now holds. Nothing else can: an entry's committed content
|
|
2775
|
+
* is resolved through the marker's own `import()`, which caches, so the
|
|
2776
|
+
* memo cannot be refreshed by re-reading.
|
|
2777
|
+
*/
|
|
2778
|
+
const jsonEntryContentsByKey = new Map();
|
|
2565
2779
|
// Entries added in this commit → their new `*.val.json` path, so later
|
|
2566
2780
|
// content ops in the same commit resolve to the freshly-created file.
|
|
2567
2781
|
const entryKeyToJsonPath = new Map();
|
|
@@ -2722,6 +2936,7 @@ class ValOps {
|
|
|
2722
2936
|
tsSourceFile = insRes.value;
|
|
2723
2937
|
tsChanged = true;
|
|
2724
2938
|
jsonEntryContents.set(jsonPath, op.value);
|
|
2939
|
+
jsonEntryContentsByKey.set(cls.entryKey, op.value);
|
|
2725
2940
|
entryKeyToJsonPath.set(cls.entryKey, jsonPath);
|
|
2726
2941
|
} else if (op.op === "remove") {
|
|
2727
2942
|
const jsonPathRes = resolveEntryJsonPath(cls.entryKey);
|
|
@@ -2739,6 +2954,7 @@ class ValOps {
|
|
|
2739
2954
|
tsSourceFile = remRes.value;
|
|
2740
2955
|
tsChanged = true;
|
|
2741
2956
|
jsonEntryContents.set(jsonPathRes.value, null);
|
|
2957
|
+
jsonEntryContentsByKey.set(cls.entryKey, null);
|
|
2742
2958
|
} else if (op.op === "replace") {
|
|
2743
2959
|
const jsonPathRes = resolveEntryJsonPath(cls.entryKey);
|
|
2744
2960
|
if (result.isErr(jsonPathRes)) {
|
|
@@ -2747,6 +2963,7 @@ class ValOps {
|
|
|
2747
2963
|
break;
|
|
2748
2964
|
}
|
|
2749
2965
|
jsonEntryContents.set(jsonPathRes.value, op.value);
|
|
2966
|
+
jsonEntryContentsByKey.set(cls.entryKey, op.value);
|
|
2750
2967
|
} else if (op.op === "move" || op.op === "copy") {
|
|
2751
2968
|
// Rename (move) or duplicate (copy) a whole entry. The new entry
|
|
2752
2969
|
// gets its own `*.val.json` written with the source entry's
|
|
@@ -2806,9 +3023,11 @@ class ValOps {
|
|
|
2806
3023
|
tsSourceFile = insRes.value;
|
|
2807
3024
|
tsChanged = true;
|
|
2808
3025
|
jsonEntryContents.set(jsonPath, content);
|
|
3026
|
+
jsonEntryContentsByKey.set(cls.entryKey, content);
|
|
2809
3027
|
entryKeyToJsonPath.set(cls.entryKey, jsonPath);
|
|
2810
3028
|
if (op.op === "move" && fromPathRes.value !== jsonPath) {
|
|
2811
3029
|
jsonEntryContents.set(fromPathRes.value, null);
|
|
3030
|
+
jsonEntryContentsByKey.set(fromKey, null);
|
|
2812
3031
|
}
|
|
2813
3032
|
} else {
|
|
2814
3033
|
errors.push({
|
|
@@ -2859,6 +3078,7 @@ class ValOps {
|
|
|
2859
3078
|
break;
|
|
2860
3079
|
}
|
|
2861
3080
|
jsonEntryContents.set(jsonPath, applied.value);
|
|
3081
|
+
jsonEntryContentsByKey.set(cls.entryKey, applied.value);
|
|
2862
3082
|
}
|
|
2863
3083
|
}
|
|
2864
3084
|
if (patchHadError) {
|
|
@@ -2929,7 +3149,8 @@ class ValOps {
|
|
|
2929
3149
|
path,
|
|
2930
3150
|
appliedPatches,
|
|
2931
3151
|
result: sourceFileText,
|
|
2932
|
-
extraFiles
|
|
3152
|
+
extraFiles,
|
|
3153
|
+
jsonEntries: Object.fromEntries(jsonEntryContentsByKey)
|
|
2933
3154
|
};
|
|
2934
3155
|
}
|
|
2935
3156
|
}
|
|
@@ -2942,6 +3163,7 @@ class ValOps {
|
|
|
2942
3163
|
errors
|
|
2943
3164
|
};
|
|
2944
3165
|
};
|
|
3166
|
+
const patchedJsonEntries = {};
|
|
2945
3167
|
const allResults = await Promise.all(Object.entries(patchesByModule).map(([path, patches]) => applySourceFilePatches(path, patches)));
|
|
2946
3168
|
let hasErrors = false;
|
|
2947
3169
|
const sourceFilePatchErrors = {};
|
|
@@ -2967,6 +3189,12 @@ class ValOps {
|
|
|
2967
3189
|
for (const [extraPath, data] of Object.entries(res.extraFiles)) {
|
|
2968
3190
|
patchedSourceFiles[extraPath] = data;
|
|
2969
3191
|
}
|
|
3192
|
+
// Kept per module and per entry key, not flattened into
|
|
3193
|
+
// `patchedSourceFiles` beside the files: a reader of an entry has a
|
|
3194
|
+
// module and a key, never a path. See `patchedJsonEntries`.
|
|
3195
|
+
if (Object.keys(res.jsonEntries).length > 0) {
|
|
3196
|
+
patchedJsonEntries[res.path] = res.jsonEntries;
|
|
3197
|
+
}
|
|
2970
3198
|
appliedPatches[res.path] = res.appliedPatches ?? [];
|
|
2971
3199
|
}
|
|
2972
3200
|
for (const patchId of res.appliedPatches ?? []) {
|
|
@@ -3007,6 +3235,7 @@ class ValOps {
|
|
|
3007
3235
|
binaryFilePatchErrors,
|
|
3008
3236
|
unappliablePatches,
|
|
3009
3237
|
patchedSourceFiles,
|
|
3238
|
+
patchedJsonEntries,
|
|
3010
3239
|
previousSourceFiles,
|
|
3011
3240
|
partiallyPatchedSourceFiles,
|
|
3012
3241
|
patchedBinaryFilesDescriptors,
|
|
@@ -3599,11 +3828,180 @@ function patchRecordFile(patchesDir, patchId) {
|
|
|
3599
3828
|
function patchBaseFile(patchesDir, patchId) {
|
|
3600
3829
|
return path__default.join(patchDir(patchesDir, patchId), "base.json");
|
|
3601
3830
|
}
|
|
3831
|
+
|
|
3832
|
+
/**
|
|
3833
|
+
* Where a patch's uploaded bytes wait for the record that will reference them.
|
|
3834
|
+
*
|
|
3835
|
+
* ## Why they cannot simply be written into the patch directory
|
|
3836
|
+
*
|
|
3837
|
+
* A patch that carries a file is written in TWO requests, and the bytes go
|
|
3838
|
+
* first: the record's `file` op holds only a sha, so a record written before its
|
|
3839
|
+
* bytes would point at nothing. Uploading straight into `<patchId>/files/` left
|
|
3840
|
+
* the directory holding files and no `patch.json` for the length of a round
|
|
3841
|
+
* trip — which is neither of the two shapes this store allows, so
|
|
3842
|
+
* {@link readPatchStore} read it as a patch whose contents were lost and repair
|
|
3843
|
+
* removed it, bytes and all.
|
|
3844
|
+
*
|
|
3845
|
+
* And that window is not passive: writing into the patches directory is exactly
|
|
3846
|
+
* what ends `getStat`'s long poll, so the upload summoned the read that
|
|
3847
|
+
* destroyed it. Replacing an image worked only when the two requests happened to
|
|
3848
|
+
* land close enough together.
|
|
3849
|
+
*
|
|
3850
|
+
* So the bytes are not in the store until they belong to something.
|
|
3851
|
+
* {@link appendPatch} moves them in after writing the record, under the lock, so
|
|
3852
|
+
* no reader ever sees a half-built patch directory — and the invariant that a
|
|
3853
|
+
* directory either holds a usable record or is named by the log holds again,
|
|
3854
|
+
* with nothing to tolerate and no ambiguous state to classify.
|
|
3855
|
+
*
|
|
3856
|
+
* A SIBLING of the patches directory, for two reasons: nothing that reads the
|
|
3857
|
+
* store lists it, and it is on the same filesystem, so moving into place is a
|
|
3858
|
+
* rename rather than a copy.
|
|
3859
|
+
*/
|
|
3860
|
+
function uploadsDir(patchesDir) {
|
|
3861
|
+
return path__default.join(path__default.dirname(patchesDir), "uploads");
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
/** Where one patch's uploads wait. See {@link uploadsDir}. */
|
|
3865
|
+
function patchUploadDir(patchesDir, patchId) {
|
|
3866
|
+
return path__default.join(uploadsDir(patchesDir), patchId);
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
/**
|
|
3870
|
+
* The binary layout, relative to whichever directory holds it.
|
|
3871
|
+
*
|
|
3872
|
+
* Shared by the patch directory and the staging directory so the two cannot
|
|
3873
|
+
* drift — a move into place has to land the bytes exactly where a read expects
|
|
3874
|
+
* them.
|
|
3875
|
+
*/
|
|
3876
|
+
function binaryFilesDir(dir) {
|
|
3877
|
+
return path__default.join(dir, "files");
|
|
3878
|
+
}
|
|
3879
|
+
function binaryFileIn(dir, filePath) {
|
|
3880
|
+
return path__default.join(binaryFilesDir(dir), filePath, path__default.basename(filePath));
|
|
3881
|
+
}
|
|
3882
|
+
function binaryFileMetadataIn(dir, filePath) {
|
|
3883
|
+
return path__default.join(binaryFilesDir(dir), filePath, "metadata.json");
|
|
3884
|
+
}
|
|
3602
3885
|
function patchBinaryFile(patchesDir, patchId, filePath) {
|
|
3603
|
-
return
|
|
3886
|
+
return binaryFileIn(patchDir(patchesDir, patchId), filePath);
|
|
3604
3887
|
}
|
|
3605
3888
|
function patchBinaryFileMetadata(patchesDir, patchId, filePath) {
|
|
3606
|
-
return
|
|
3889
|
+
return binaryFileMetadataIn(patchDir(patchesDir, patchId), filePath);
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
/** The staged twin of {@link patchBinaryFile}. */
|
|
3893
|
+
function stagedPatchBinaryFile(patchesDir, patchId, filePath) {
|
|
3894
|
+
return binaryFileIn(patchUploadDir(patchesDir, patchId), filePath);
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
/** The staged twin of {@link patchBinaryFileMetadata}. */
|
|
3898
|
+
function stagedPatchBinaryFileMetadata(patchesDir, patchId, filePath) {
|
|
3899
|
+
return binaryFileMetadataIn(patchUploadDir(patchesDir, patchId), filePath);
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
/**
|
|
3903
|
+
* Move a patch's staged uploads into the patch directory.
|
|
3904
|
+
*
|
|
3905
|
+
* Called by {@link appendPatch} between the record and the log line, so it runs
|
|
3906
|
+
* under the lock and no reader can observe the halfway state.
|
|
3907
|
+
*
|
|
3908
|
+
* The whole `files` tree in one rename where it can be — the common case, since
|
|
3909
|
+
* a patch's files only ever arrive before its record — and per file otherwise,
|
|
3910
|
+
* for the case where something is already there.
|
|
3911
|
+
*/
|
|
3912
|
+
function moveStagedUploadsIn(patchesDir, patchId) {
|
|
3913
|
+
const from = binaryFilesDir(patchUploadDir(patchesDir, patchId));
|
|
3914
|
+
if (!fs.existsSync(from)) {
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
const to = binaryFilesDir(patchDir(patchesDir, patchId));
|
|
3918
|
+
if (!fs.existsSync(to)) {
|
|
3919
|
+
fs.mkdirSync(path__default.dirname(to), {
|
|
3920
|
+
recursive: true
|
|
3921
|
+
});
|
|
3922
|
+
fs.renameSync(from, to);
|
|
3923
|
+
} else {
|
|
3924
|
+
moveTreeInto(from, to);
|
|
3925
|
+
}
|
|
3926
|
+
removeStagedUploads(patchesDir, patchId);
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
/** File-by-file, for when the destination already holds some of the tree. */
|
|
3930
|
+
function moveTreeInto(from, to) {
|
|
3931
|
+
for (const entry of fs.readdirSync(from, {
|
|
3932
|
+
withFileTypes: true
|
|
3933
|
+
})) {
|
|
3934
|
+
const source = path__default.join(from, entry.name);
|
|
3935
|
+
const target = path__default.join(to, entry.name);
|
|
3936
|
+
if (entry.isDirectory()) {
|
|
3937
|
+
fs.mkdirSync(target, {
|
|
3938
|
+
recursive: true
|
|
3939
|
+
});
|
|
3940
|
+
moveTreeInto(source, target);
|
|
3941
|
+
continue;
|
|
3942
|
+
}
|
|
3943
|
+
fs.mkdirSync(path__default.dirname(target), {
|
|
3944
|
+
recursive: true
|
|
3945
|
+
});
|
|
3946
|
+
fs.renameSync(source, target);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
/** Drop a patch's staging directory, whatever is left of it. */
|
|
3951
|
+
function removeStagedUploads(patchesDir, patchId) {
|
|
3952
|
+
try {
|
|
3953
|
+
fs.rmSync(patchUploadDir(patchesDir, patchId), {
|
|
3954
|
+
recursive: true,
|
|
3955
|
+
force: true
|
|
3956
|
+
});
|
|
3957
|
+
} catch {
|
|
3958
|
+
// Hygiene, not correctness: nothing reads a staging directory that no patch
|
|
3959
|
+
// claims, and the sweep below gets it eventually.
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
/**
|
|
3964
|
+
* How long an upload nobody claimed is kept.
|
|
3965
|
+
*
|
|
3966
|
+
* Only garbage collection, which is why it can be a guess at all: these bytes
|
|
3967
|
+
* are outside the store, so no reader can mistake them for a patch and nothing
|
|
3968
|
+
* is lost by keeping them a while. The old marker-based attempt at this problem
|
|
3969
|
+
* had a TTL deciding whether to delete something INSIDE the store, where being
|
|
3970
|
+
* wrong meant destroying a live upload.
|
|
3971
|
+
*/
|
|
3972
|
+
const STALE_UPLOAD_MS = 24 * 60 * 60 * 1000;
|
|
3973
|
+
|
|
3974
|
+
/**
|
|
3975
|
+
* Drop staged uploads whose patch never arrived.
|
|
3976
|
+
*
|
|
3977
|
+
* A client that dies between the upload and the `PUT` leaves its bytes here.
|
|
3978
|
+
* Nothing references them — no record points at them and the log never named
|
|
3979
|
+
* them — so they are removed without a word.
|
|
3980
|
+
*/
|
|
3981
|
+
function sweepStaleUploads(patchesDir, now = Date.now()) {
|
|
3982
|
+
const dir = uploadsDir(patchesDir);
|
|
3983
|
+
if (!fs.existsSync(dir)) {
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3986
|
+
let names;
|
|
3987
|
+
try {
|
|
3988
|
+
names = fs.readdirSync(dir);
|
|
3989
|
+
} catch {
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
for (const name of names) {
|
|
3993
|
+
const staged = path__default.join(dir, name);
|
|
3994
|
+
try {
|
|
3995
|
+
if (now - fs.statSync(staged).mtimeMs < STALE_UPLOAD_MS) continue;
|
|
3996
|
+
fs.rmSync(staged, {
|
|
3997
|
+
recursive: true,
|
|
3998
|
+
force: true
|
|
3999
|
+
});
|
|
4000
|
+
} catch {
|
|
4001
|
+
// Someone else is writing here, or it is already gone. Either way it is
|
|
4002
|
+
// not this pass's business.
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
3607
4005
|
}
|
|
3608
4006
|
|
|
3609
4007
|
/** Names that live in the patches directory but are not patches. */
|
|
@@ -3839,6 +4237,18 @@ function writePatchRecord(patchesDir, patchId, record) {
|
|
|
3839
4237
|
function appendPatch(patchesDir, record) {
|
|
3840
4238
|
const patchId = record.patchId;
|
|
3841
4239
|
writePatchRecord(patchesDir, patchId, record);
|
|
4240
|
+
/*
|
|
4241
|
+
* Then the bytes, then the log line — and the order is the whole point.
|
|
4242
|
+
*
|
|
4243
|
+
* The record goes first, so the directory never exists without one: that is
|
|
4244
|
+
* what makes "files but no patch.json" a state this store cannot produce, and
|
|
4245
|
+
* what lets a reader keep treating it as a patch whose contents are lost.
|
|
4246
|
+
* The log line goes last, so an interrupted append leaves a directory the log
|
|
4247
|
+
* does not name — the benign half of a crash, swept silently.
|
|
4248
|
+
*
|
|
4249
|
+
* See `uploadsDir`. Under the lock, like the rest of this function.
|
|
4250
|
+
*/
|
|
4251
|
+
moveStagedUploadsIn(patchesDir, patchId);
|
|
3842
4252
|
const entry = {
|
|
3843
4253
|
patchId,
|
|
3844
4254
|
createdAt: record.createdAt,
|
|
@@ -4488,13 +4898,24 @@ class ValOpsFS extends ValOps {
|
|
|
4488
4898
|
};
|
|
4489
4899
|
}
|
|
4490
4900
|
const patches = announceRes.entries.map(entry => entry.patchId);
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4901
|
+
/**
|
|
4902
|
+
* Drained on the channel that always flows.
|
|
4903
|
+
*
|
|
4904
|
+
* A repair that removed everything leaves the studio nothing to fetch, so
|
|
4905
|
+
* a notice riding on `GET /patches` would sit here unread.
|
|
4906
|
+
*
|
|
4907
|
+
* Accumulating rather than assigning, because the long poll below drains
|
|
4908
|
+
* again before it answers: a repair during the wait must not have to sit
|
|
4909
|
+
* out another whole stat.
|
|
4910
|
+
*/
|
|
4911
|
+
const removed = [];
|
|
4912
|
+
const drainRemoved = () => {
|
|
4913
|
+
removed.push(...this.removedPatchNotices.splice(0, this.removedPatchNotices.length));
|
|
4914
|
+
return removed.length > 0 ? {
|
|
4915
|
+
removed
|
|
4916
|
+
} : {};
|
|
4917
|
+
};
|
|
4918
|
+
const removedNotice = drainRemoved();
|
|
4498
4919
|
// something changed: return immediately
|
|
4499
4920
|
const didChange = !params ||
|
|
4500
4921
|
// An entry file changed on disk: nothing else here can see that, since a
|
|
@@ -4575,6 +4996,14 @@ class ValOpsFS extends ValOps {
|
|
|
4575
4996
|
if (Date.now() - start > interval) {
|
|
4576
4997
|
console.warn("Val: polling interval of files exceeded");
|
|
4577
4998
|
}
|
|
4999
|
+
// Checked BEFORE rescheduling, as the patches-directory poller
|
|
5000
|
+
// above does. Without it this walk goes on forever: the `finally`
|
|
5001
|
+
// that ends the race clears the handle it can see, and a `go`
|
|
5002
|
+
// already on the queue then schedules one nothing will ever clear —
|
|
5003
|
+
// a leaked timer, re-stat-ing the whole project, per stat poll.
|
|
5004
|
+
if (stopPolling) {
|
|
5005
|
+
return;
|
|
5006
|
+
}
|
|
4578
5007
|
setHandle(setTimeout(() => go(resolve), interval));
|
|
4579
5008
|
};
|
|
4580
5009
|
if (stopPolling) {
|
|
@@ -4587,6 +5016,11 @@ class ValOpsFS extends ValOps {
|
|
|
4587
5016
|
const disableFilePolling = ((_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.disableFilePolling) || false;
|
|
4588
5017
|
let patchesDirHandle;
|
|
4589
5018
|
let valFilesIntervalHandle;
|
|
5019
|
+
// Held so the `finally` can clear it. A race the timeout LOSES still
|
|
5020
|
+
// leaves its timer armed, and it is the long one — so every stat that
|
|
5021
|
+
// returned on a file change kept the whole poll interval alive behind it,
|
|
5022
|
+
// doing nothing but holding the event loop open.
|
|
5023
|
+
let noChangeHandle;
|
|
4590
5024
|
const type = await Promise.race([
|
|
4591
5025
|
// we poll the patches directory for changes since fs.watch does not work reliably on all system (in particular on WSL) and just checking the patches dir is relatively cheap
|
|
4592
5026
|
disableFilePolling ? new Promise(() => {}) : didDirectoryChangeUsingPolling(this.getPatchesDir(), statFilePollingInterval, handle => {
|
|
@@ -4619,7 +5053,7 @@ class ValOpsFS extends ValOps {
|
|
|
4619
5053
|
});
|
|
4620
5054
|
}), new Promise(resolve => {
|
|
4621
5055
|
var _this$options3;
|
|
4622
|
-
|
|
5056
|
+
noChangeHandle = setTimeout(() => resolve("no-change"), ((_this$options3 = this.options) === null || _this$options3 === void 0 ? void 0 : _this$options3.statPollingInterval) || 20000);
|
|
4623
5057
|
})]).finally(() => {
|
|
4624
5058
|
if (fsWatcher) {
|
|
4625
5059
|
fsWatcher.close();
|
|
@@ -4627,14 +5061,52 @@ class ValOpsFS extends ValOps {
|
|
|
4627
5061
|
stopPolling = true;
|
|
4628
5062
|
clearInterval(patchesDirHandle);
|
|
4629
5063
|
clearInterval(valFilesIntervalHandle);
|
|
5064
|
+
clearTimeout(noChangeHandle);
|
|
4630
5065
|
});
|
|
5066
|
+
/**
|
|
5067
|
+
* Read the store AGAIN, because `patches` above describes the moment this
|
|
5068
|
+
* poll OPENED — up to a whole polling interval ago.
|
|
5069
|
+
*
|
|
5070
|
+
* That staleness is not a detail: what ends the wait is a write, and the
|
|
5071
|
+
* write the studio does most is `/save`, which in `fs` mode commits the
|
|
5072
|
+
* patches and DELETES them. Answering with the list read before it names
|
|
5073
|
+
* patches that no longer exist, and the studio then puts those ids back in
|
|
5074
|
+
* its chain and fetches them from a server that correctly no longer has
|
|
5075
|
+
* them — "unpublished changes could not be loaded", for changes that were
|
|
5076
|
+
* published a moment earlier. With auto-save on, that is every pause in
|
|
5077
|
+
* typing.
|
|
5078
|
+
*
|
|
5079
|
+
* So a stat describes the moment it ANSWERS. `request-again` and
|
|
5080
|
+
* `no-change` differ in why the wait ended, not in how current the answer
|
|
5081
|
+
* has to be, so both come through here.
|
|
5082
|
+
*
|
|
5083
|
+
* The patch list is the only part that CAN have moved: the shas and the
|
|
5084
|
+
* schemas come from `initSources`, which is memoised for the lifetime of
|
|
5085
|
+
* this instance and never invalidated — a module change makes a new
|
|
5086
|
+
* `ValOpsFS` rather than updating this one. Recomputing them would be a
|
|
5087
|
+
* second full schema serialization per poll for a value that cannot have
|
|
5088
|
+
* changed.
|
|
5089
|
+
*
|
|
5090
|
+
* A read error is returned as one rather than papered over with the list
|
|
5091
|
+
* from the open: a patch store that cannot be read is what the error path
|
|
5092
|
+
* is for, and the studio asks again.
|
|
5093
|
+
*/
|
|
5094
|
+
const answerRes = await this.readStore();
|
|
5095
|
+
if (answerRes.status === "error") {
|
|
5096
|
+
return {
|
|
5097
|
+
type: "error",
|
|
5098
|
+
error: {
|
|
5099
|
+
message: answerRes.message
|
|
5100
|
+
}
|
|
5101
|
+
};
|
|
5102
|
+
}
|
|
4631
5103
|
return {
|
|
4632
5104
|
type,
|
|
4633
5105
|
baseSha: currentBaseSha,
|
|
4634
5106
|
schemaSha: currentSchemaSha,
|
|
4635
5107
|
sourcesSha: currentSourcesSha,
|
|
4636
|
-
patches,
|
|
4637
|
-
...
|
|
5108
|
+
patches: answerRes.entries.map(entry => entry.patchId),
|
|
5109
|
+
...drainRemoved(),
|
|
4638
5110
|
jsonEntriesSha: currentJsonEntriesSha
|
|
4639
5111
|
};
|
|
4640
5112
|
} catch (err) {
|
|
@@ -4966,14 +5438,28 @@ class ValOpsFS extends ValOps {
|
|
|
4966
5438
|
}
|
|
4967
5439
|
async saveBase64EncodedBinaryFileFromPatch(filePath, _parentRef, patchId, data, _type, metadata) {
|
|
4968
5440
|
// Keyed by the patch's own id, so the parent is not needed and is not asked
|
|
4969
|
-
// for.
|
|
4970
|
-
//
|
|
4971
|
-
// and
|
|
5441
|
+
// for.
|
|
5442
|
+
//
|
|
5443
|
+
// Written OUTSIDE the store, and moved in by `appendPatch` once the record
|
|
5444
|
+
// exists. Uploads arrive before the patch record does — the record's `file`
|
|
5445
|
+
// op carries only a sha, so it would otherwise point at nothing — and
|
|
5446
|
+
// writing them straight into `<patchId>/files/` left a directory holding
|
|
5447
|
+
// files and no `patch.json` for a whole round trip. Nothing can read that as
|
|
5448
|
+
// anything but a patch whose contents are lost, so repair removed it, bytes
|
|
5449
|
+
// and all, and the image 404ed.
|
|
5450
|
+
//
|
|
5451
|
+
// Staging is what makes that state unreachable rather than tolerated. See
|
|
5452
|
+
// `uploadsDir`.
|
|
4972
5453
|
const patchesDir = this.getPatchesDir();
|
|
4973
|
-
const patchFilePath =
|
|
4974
|
-
const metadataFilePath =
|
|
5454
|
+
const patchFilePath = stagedPatchBinaryFile(patchesDir, patchId, filePath);
|
|
5455
|
+
const metadataFilePath = stagedPatchBinaryFileMetadata(patchesDir, patchId, filePath);
|
|
4975
5456
|
try {
|
|
4976
5457
|
if (data === null) {
|
|
5458
|
+
// A delete is the other order — the record is written first — so the
|
|
5459
|
+
// bytes are already in the store. Both locations, because a file staged
|
|
5460
|
+
// and then removed before its record never got there.
|
|
5461
|
+
this.host.deleteFile(patchBinaryFile(patchesDir, patchId, filePath));
|
|
5462
|
+
this.host.deleteFile(patchBinaryFileMetadata(patchesDir, patchId, filePath));
|
|
4977
5463
|
this.host.deleteFile(patchFilePath);
|
|
4978
5464
|
this.host.deleteFile(metadataFilePath);
|
|
4979
5465
|
return {
|
|
@@ -4981,6 +5467,9 @@ class ValOpsFS extends ValOps {
|
|
|
4981
5467
|
filePath
|
|
4982
5468
|
};
|
|
4983
5469
|
}
|
|
5470
|
+
// Cheap, and this is the one path that creates staging directories, so it
|
|
5471
|
+
// is where the ones nobody claimed get noticed.
|
|
5472
|
+
sweepStaleUploads(patchesDir);
|
|
4984
5473
|
const buffer = bufferFromDataUrl(data);
|
|
4985
5474
|
if (!buffer) {
|
|
4986
5475
|
return {
|
|
@@ -5010,9 +5499,28 @@ class ValOpsFS extends ValOps {
|
|
|
5010
5499
|
};
|
|
5011
5500
|
}
|
|
5012
5501
|
}
|
|
5502
|
+
|
|
5503
|
+
/**
|
|
5504
|
+
* Which of the two places a patch's file can be, if either.
|
|
5505
|
+
*
|
|
5506
|
+
* The bytes are in the store once the patch's record is, and in the staging
|
|
5507
|
+
* area before that — see `uploadsDir`. Every reader has to accept both, and
|
|
5508
|
+
* they decide it here rather than each on its own, so two readers of the same
|
|
5509
|
+
* file cannot disagree about whether it exists.
|
|
5510
|
+
*/
|
|
5511
|
+
wherePatchFileIs(inStore, staged) {
|
|
5512
|
+
if (this.host.fileExists(inStore)) {
|
|
5513
|
+
return inStore;
|
|
5514
|
+
}
|
|
5515
|
+
if (this.host.fileExists(staged)) {
|
|
5516
|
+
return staged;
|
|
5517
|
+
}
|
|
5518
|
+
return null;
|
|
5519
|
+
}
|
|
5013
5520
|
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
|
|
5014
|
-
const
|
|
5015
|
-
|
|
5521
|
+
const patchesDir = this.getPatchesDir();
|
|
5522
|
+
const metadataFilePath = this.wherePatchFileIs(patchBinaryFileMetadata(patchesDir, patchId, filePath), stagedPatchBinaryFileMetadata(patchesDir, patchId, filePath));
|
|
5523
|
+
if (metadataFilePath === null) {
|
|
5016
5524
|
return {
|
|
5017
5525
|
errors: [{
|
|
5018
5526
|
message: "Metadata file not found",
|
|
@@ -5049,8 +5557,9 @@ class ValOpsFS extends ValOps {
|
|
|
5049
5557
|
async getBase64EncodedBinaryFileFromPatch(filePath, patchId) {
|
|
5050
5558
|
// Straight from the id. This used to read and parse every patch on disk to
|
|
5051
5559
|
// work out which directory the file was under, on every single image request.
|
|
5052
|
-
const
|
|
5053
|
-
|
|
5560
|
+
const patchesDir = this.getPatchesDir();
|
|
5561
|
+
const absPath = this.wherePatchFileIs(patchBinaryFile(patchesDir, patchId, filePath), stagedPatchBinaryFile(patchesDir, patchId, filePath));
|
|
5562
|
+
if (absPath === null) {
|
|
5054
5563
|
return null;
|
|
5055
5564
|
}
|
|
5056
5565
|
return this.host.readBinaryFile(absPath);
|
|
@@ -5095,6 +5604,9 @@ class ValOpsFS extends ValOps {
|
|
|
5095
5604
|
recursive: true,
|
|
5096
5605
|
force: true
|
|
5097
5606
|
});
|
|
5607
|
+
// And anything this patch had staged but never moved in, which is
|
|
5608
|
+
// the case where a delete arrives before the record does.
|
|
5609
|
+
removeStagedUploads(patchesDir, patchId);
|
|
5098
5610
|
deleted.push(patchId);
|
|
5099
5611
|
} catch (err) {
|
|
5100
5612
|
// Reported. This endpoint used to answer "deleted" unconditionally —
|
|
@@ -8325,6 +8837,26 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
8325
8837
|
}
|
|
8326
8838
|
};
|
|
8327
8839
|
}
|
|
8840
|
+
/*
|
|
8841
|
+
* The files on disk are the committed content now, so say so here too.
|
|
8842
|
+
*
|
|
8843
|
+
* Nothing else will: the sources are memoised per `ValOps` instance
|
|
8844
|
+
* and are re-read by awaiting each module's `def`, which is the app's
|
|
8845
|
+
* own `import()` — that resolves from the module registry, not from
|
|
8846
|
+
* the file this save just rewrote. So until the host rebuilds its
|
|
8847
|
+
* module graph, every read of committed content answers with what was
|
|
8848
|
+
* there before the save. A page rendering draft content sees exactly
|
|
8849
|
+
* that once the patches below are gone: `getJsonEntry` resolves the
|
|
8850
|
+
* committed entry and has no patches left to replay over it.
|
|
8851
|
+
*
|
|
8852
|
+
* Before `deletePatches`, because it needs the patches it is adopting
|
|
8853
|
+
* the result of. See `ValOps.promoteCommittedSources` for why the SHAs
|
|
8854
|
+
* move with them.
|
|
8855
|
+
*/
|
|
8856
|
+
await serverOps.adoptCommittedSources({
|
|
8857
|
+
...analysis,
|
|
8858
|
+
...patches
|
|
8859
|
+
}, preparedCommit);
|
|
8328
8860
|
/*
|
|
8329
8861
|
* Only what this request consumed.
|
|
8330
8862
|
*
|