# tree reification

For the tree:

```
a
+-- b
|   +-- c
|   +-- d
|   |   +-- e
|   +-- f
+-- x
|   +-- y
|       +-- z
+-- p
    +-- q
```

Turn it into:

```
a
+-- b'
|   +-- c'
|   +-- d
|   |   +-- e'
|   +-- f
+-- x
|   +-- y'
|       +-- z
+-- i
    +-- j
```

That is, update b, c, e, and y, but not d, f, x, or z; delete p and q, and
add i and j.

## Considerations

- Windows will fail with EPERM when trying to move or delete a folder with
  current (or even recent) file operations happening inside it.
- Nodes with `bundleDependencies` _also_ bundle the meta-deps of their
  bundled deps.  But, we don't know what those are from the manifest, and
  it's possible that there's overlap between bundled metadeps and regular
  dependencies, meaning that there can be extraneous nodes in the ideal
  tree once all the tarballs are unpacked.
- This is exceedingly time-sensitive.
- Users generally expect that a failed install should not make their app
  unusable.

## Reify-A: Safe Rollback-able Process

This algorithm avoids ever renaming a directory, or removing a directory
with recent writes (except in the case of failure rollbacks), so as to
minimize the chances of hitting Windows file-locking EPERM issues.

It is very safe, and somewhat disk-inefficient.

### step 1: retire shallowest nodes to be replaced

Move aside the nodes that will be replaced.  They'll be removed if all goes
well, but if there's an error, we'll move them back.

(Note: `.b-hash` is actually something like `.b-<8 chars of base64 sha1>`.
We could probably do this _slightly_ faster if we didn't hash the folder,
and instead used a name like `.b-retired`, but not sure if it's worth
it?  Seems like it _should_ be safe from collisions?  If we're gonna hash
it to defend against concurrent reification commands, then it ought to
include the process id or something, which it currently doesn't, and limits
our options in the future.)

```
a
+-- b (.b-hash)
|   +-- c
|   +-- d
|   |   +-- e
|   +-- f
+-- x
|   +-- y (.y-hash)
|       +-- z
+-- p (.p-hash)
    +-- q
```

Fail: rename each retired `.${name}-hash` folder back to `${name}`

### step 2: create sparse tree

Now that the shallowest changing nodes are retired, `mkdir` all leaf
nodes.

```
a
+-- b (.b-hash)
|   +-- c
|   +-- d
|   |   +-- e
|   +-- f
+-- b (empty)
|   +-- c (empty)
|   +-- d (empty)
|       +-- e (empty)
+-- x
|   +-- y (.y-hash)
|   |   +-- z
|   +-- y (empty)
+-- p (.p-hash)
|   +-- q
+-- i (empty)
    +-- j (empty)
```

Fail: rimraf sparse tree, fail step 1

### step 3: unpack bundled deps into sparse tree and update

1. Group all nodes with bundles by depth.
2. For depth = 0 to max, for all nodes at that depth with bundleDeps,
    1. If no nodes at that level with bundles still in tree, done
    2. extract all nodes with bundles at that depth level in parallel
    3. call `loadActual` on each one, and move each of the actual subtree's
       children under the node in the ideal tree.
3. If any bundleDeps unpacked, prune tree

Fail: fail step 2

### step 5: unpack sparse tree of nodes changing

```
a
+-- b (.b-hash)
|   +-- c
|   +-- d
|   |   +-- e
|   +-- f
+-- b'
|   +-- c'
|   +-- d (empty)
|       +-- e'
+-- x
|   +-- y (.y-hash)
|   |   +-- z
|   +-- y'
+-- p (.p-hash)
|   +-- q
+-- i
    +-- j
```

To maximize parallelization while minimizing unnecessary fetches for
bundled deps and meta-deps:

For all diff nodes in parallel, `pacote.extract` if add/change.

Fail: fail step 2

### step 6: move unchanging nodes into sparse tree

```
a
+-- b (.b-hash)
|   +-- c
|   +-- d (empty)
|       +-- e
+-- b'
|   +-- c'
|   +-- d
|   |   +-- e'
|   +-- f
+-- x
|   +-- y (.y-hash)
|   +-- y'
|       +-- z
+-- p (.p-hash)
|   +-- q
+-- i
    +-- j
```

This actually means that we move each unchanging node's _contents_ (other
than `node_modules`) into the new location.  (Maybe we ought to _only_ ever
move files, not directories?)

Fail: move unchanging nodes back to retired tree, fail step 2

**Windows Consideration!** Extremely easy for a failure in this step to
lead to EPERM in the rollback, if we try to rimraf the sparse tree before
we're fully moved out of it.

### step 7: rimraf retired original (now sparse) nodes and removal nodes

```
a
+-- b'
|   +-- c'
|   +-- d
|   |   +-- e'
|   +-- f
+-- x
|   +-- y'
|       +-- z
+-- i
    +-- j
```

Fail: report failure as a warning and instruct user to `rm -rf` the garbage
directory.

## Reify-B: Fast and Dirty Approach

This is the fastest and most efficient way to proceed, but does not admit
any reasonable rollback approach, since we fully delete existing packages
_before_ unpacking the new ones.

### step 1: clear away excess

Scan actualTree inventory.  For each node:

- If not in idealTree, rimraf path, remove from actualTree (so we don't
  check its children).
- If in ideal tree and identical resolved/integrity, leave it alone.
- If in ideal tree and different, and no unbundled children, rimraf path,
  remove from actualTree
- If in ideal tree and different and has unbundled children,
    - rimraf package contents
    - rimraf bundled children and remove them from tree

### step 2: lay the path

`mkdir` every path in ideal tree that does not exist in actualTree

### step 3: extract bundlers

1. Group all nodes with bundles by depth.
2. for depth = 0, for all nodes at that depth with bundleDependencies,
    1. If no nodes at that level with bundles still in the tree, done.
    2. extract all nodes with bundles at that depth level in parallel
    3. call `loadActual` with a new Arborist on each one, and move each
       depth=1 child node into the ideal tree.
3. if any bundle deps unpacked, prune tree

### step 4: extract all others

Extract all remaining packages in parallel.

Their folders already exist, so it's fine to just dump them all into place.
