@adea-ai/plugins 3.0.10 → 4.0.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.
Files changed (75) hide show
  1. package/README.md +18 -11
  2. package/dist/agent-plugins-catalog.d.ts +2 -3
  3. package/dist/agent-plugins-catalog.js +2 -10
  4. package/dist/agent-plugins-catalog.js.map +1 -1
  5. package/dist/catalog-store.d.ts +81 -0
  6. package/dist/catalog-store.js +487 -0
  7. package/dist/catalog-store.js.map +1 -0
  8. package/dist/consumer-index.d.ts +1 -1
  9. package/dist/consumer-index.js +17 -7
  10. package/dist/consumer-index.js.map +1 -1
  11. package/dist/display-name.js +3 -2
  12. package/dist/display-name.js.map +1 -1
  13. package/dist/harness.d.ts +50 -9
  14. package/dist/harness.js +190 -147
  15. package/dist/harness.js.map +1 -1
  16. package/dist/icon-mirror.d.ts +2 -2
  17. package/dist/icon-mirror.js +38 -25
  18. package/dist/icon-mirror.js.map +1 -1
  19. package/dist/icons.js +5 -27
  20. package/dist/icons.js.map +1 -1
  21. package/dist/index.d.ts +9 -3
  22. package/dist/index.js +103 -88
  23. package/dist/index.js.map +1 -1
  24. package/dist/publication.d.ts +8 -11
  25. package/dist/publication.js +8 -11
  26. package/dist/publication.js.map +1 -1
  27. package/dist/schema.d.ts +0 -40
  28. package/dist/schema.js +0 -36
  29. package/dist/schema.js.map +1 -1
  30. package/dist/site-icons.d.ts +2 -0
  31. package/dist/site-icons.js +46 -9
  32. package/dist/site-icons.js.map +1 -1
  33. package/dist/source-digest.d.ts +2 -0
  34. package/dist/source-digest.js +12 -0
  35. package/dist/source-digest.js.map +1 -0
  36. package/dist/sources.js +3 -3
  37. package/dist/sources.js.map +1 -1
  38. package/native/build-provenance.txt +5 -5
  39. package/native/staged-reader-win32-arm64.node +0 -0
  40. package/native/staged-reader-win32-x64.node +0 -0
  41. package/package.json +4 -9
  42. package/src/agent-plugins-catalog.ts +3 -10
  43. package/src/artifacts.test.ts +80 -0
  44. package/src/catalog-installation.test.ts +1 -1
  45. package/src/catalog-store.ts +546 -0
  46. package/src/consumer-index.test.ts +125 -0
  47. package/src/consumer-index.ts +23 -8
  48. package/src/default-head.test.ts +143 -0
  49. package/src/display-name.test.ts +8 -0
  50. package/src/display-name.ts +3 -2
  51. package/src/{agent-plugins-harness.test.ts → harness.test.ts} +1 -1
  52. package/src/harness.ts +249 -170
  53. package/src/icon-mirror.ts +36 -23
  54. package/src/icons.test.ts +5 -0
  55. package/src/icons.ts +6 -29
  56. package/src/index.test.ts +272 -40
  57. package/src/index.ts +131 -94
  58. package/src/manifest-retry.test.ts +49 -0
  59. package/src/portable-catalog.test.ts +8 -5
  60. package/src/publication.ts +8 -11
  61. package/src/schema.ts +0 -40
  62. package/src/site-icon-fallback.test.ts +36 -19
  63. package/src/site-icons.test.ts +104 -0
  64. package/src/site-icons.ts +47 -9
  65. package/src/source-config-sync.test.ts +84 -0
  66. package/src/source-digest.test.ts +101 -0
  67. package/src/source-digest.ts +14 -0
  68. package/src/sources-json.test.ts +53 -0
  69. package/src/sources.ts +3 -3
  70. package/src/upstream-headers.test.ts +31 -7
  71. package/src/write-artifacts.test.ts +375 -0
  72. package/dist/agent-plugins-harness.d.ts +0 -54
  73. package/dist/agent-plugins-harness.js +0 -212
  74. package/dist/agent-plugins-harness.js.map +0 -1
  75. package/src/agent-plugins-harness.ts +0 -281
package/dist/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { promises as fs } from 'node:fs';
4
- import { basename, dirname, join, relative, resolve } from 'node:path';
4
+ import { basename, join, relative, resolve } from 'node:path';
5
5
  import { Readable } from 'node:stream';
6
+ import { setTimeout as sleep } from 'node:timers/promises';
6
7
  import { createGunzip } from 'node:zlib';
7
8
  import { extract } from 'tar-stream';
9
+ import { publishCatalog } from './catalog-store.js';
8
10
  import { CatalogSchema, HarnessSchema, SourcesLockSchema, PluginSchema, } from './schema.js';
9
11
  import { canonicalRepositoryUrl, parseJsonDocument, parseMarketplaceManifest, safeRelativePath, } from './sources.js';
10
12
  import { buildConsumerIndex, CATALOG_INDEX_ARTIFACT, iconCoverage, } from './consumer-index.js';
@@ -13,6 +15,9 @@ import { stageIconBytes } from './icon-mirror.js';
13
15
  import { isVendorHomepage, resolveSiteIcon, } from './site-icons.js';
14
16
  import { normalizeDisplayName } from './display-name.js';
15
17
  import { immutableAssetUrl } from './publication.js';
18
+ import { sourceDigest as bytesDigest } from './source-digest.js';
19
+ export { bytesDigest };
20
+ export { catalogFingerprint, publishCatalog, withCatalogRead, writeFileAtomically, } from './catalog-store.js';
16
21
  /**
17
22
  * Marks the catalog format this compiler emits.
18
23
  *
@@ -55,14 +60,6 @@ export function canonicalJson(value) {
55
60
  export function digest(value) {
56
61
  return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
57
62
  }
58
- export function bytesDigest(files) {
59
- const hash = createHash('sha256');
60
- for (const [path, bytes] of [...files.entries()].toSorted(([left], [right]) => left.localeCompare(right))) {
61
- hash.update(`${path.length}:${path}:${bytes.byteLength}:`);
62
- hash.update(bytes);
63
- }
64
- return `sha256:${hash.digest('hex')}`;
65
- }
66
63
  export function stablePluginId(sourceId, upstreamName) {
67
64
  return `plugin:${slug(sourceId)}:${slug(upstreamName)}`;
68
65
  }
@@ -88,17 +85,22 @@ export async function synchronize(input) {
88
85
  sourceId: config.sourceId,
89
86
  commitSha,
90
87
  }));
88
+ const resolvedLock = createSourcesLock(resolved);
91
89
  const unchanged = input.fromLock === undefined &&
92
90
  input.forceRebuild !== true &&
93
91
  input.existingLock !== undefined &&
94
92
  resolved.length === input.existingLock.sources.length &&
95
- resolved.every((source) => {
96
- const previous = input.existingLock?.sources.find((entry) => entry.sourceId === source.config.sourceId);
97
- if (!previous)
93
+ resolvedLock.sources.every((source) => {
94
+ const previous = input.existingLock?.sources.find((entry) => entry.sourceId === source.sourceId);
95
+ if (!previous || sourceLockChanged(previous, source))
98
96
  return false;
99
- return (previous.resolvedCommitSha === source.commitSha &&
100
- previous.sourceManifestDigest === source.manifestDigest &&
101
- digest(previous.pluginPins) === digest(source.pluginPins));
97
+ if (!input.existingCatalog)
98
+ return true;
99
+ const priorCatalog = input.existingCatalog.sources.find((entry) => entry.sourceId === source.sourceId);
100
+ const config = resolved.find((entry) => entry.config.sourceId === source.sourceId).config;
101
+ return (!!priorCatalog &&
102
+ priorCatalog.displayName === config.displayName &&
103
+ priorCatalog.trustClassification === config.trustClassification);
102
104
  });
103
105
  if (unchanged) {
104
106
  return {
@@ -135,7 +137,7 @@ export async function synchronize(input) {
135
137
  ...(input.transformRelease ? { transformRelease: input.transformRelease } : {}),
136
138
  });
137
139
  const catalog = buildResult.catalog;
138
- const lock = createSourcesLock(resolved);
140
+ const lock = resolvedLock;
139
141
  const artifacts = createArtifacts(orderLeadingPlugins(catalog, input.leading), lock, {
140
142
  ...(input.leading ? { leading: input.leading } : {}),
141
143
  ...(input.topCount !== undefined ? { topCount: input.topCount } : {}),
@@ -179,6 +181,7 @@ export async function buildCatalog(input) {
179
181
  ...(input.productCategories ? { productCategories: input.productCategories } : {}),
180
182
  ...(input.leading ? { leading: input.leading } : {}),
181
183
  ...(input.productIconOverrides ? { productIconOverrides: input.productIconOverrides } : {}),
184
+ ...(input.iconAssetsDirectory ? { iconAssetsDirectory: input.iconAssetsDirectory } : {}),
182
185
  })).catalog;
183
186
  }
184
187
  async function buildCatalogInternal(input) {
@@ -583,7 +586,7 @@ async function resolvePluginSource(pluginName, spec, source, resolveExternalRefs
583
586
  const commitSha = pin?.resolvedCommitSha ??
584
587
  spec.sha ??
585
588
  (resolveExternalRefs
586
- ? await resolveGitRef(spec.repositoryUrl, spec.ref?.startsWith('refs/') ? spec.ref : `refs/heads/${spec.ref ?? 'main'}`)
589
+ ? await resolveGitRef(spec.repositoryUrl, externalGitRef(spec.ref))
587
590
  : source.commitSha);
588
591
  return {
589
592
  repositoryUrl: canonicalUrl,
@@ -790,6 +793,15 @@ function findLicense(entry, metadata) {
790
793
  source: metadata?.license !== undefined ? 'plugin-manifest' : 'marketplace-entry',
791
794
  };
792
795
  }
796
+ function sourceLockChanged(previous, current) {
797
+ return (previous.repositoryUrl !== current.repositoryUrl ||
798
+ previous.defaultBranch !== current.defaultBranch ||
799
+ previous.manifestPath !== current.manifestPath ||
800
+ previous.marketplaceDialect !== current.marketplaceDialect ||
801
+ previous.resolvedCommitSha !== current.resolvedCommitSha ||
802
+ previous.sourceManifestDigest !== current.sourceManifestDigest ||
803
+ digest(previous.pluginPins) !== digest(current.pluginPins));
804
+ }
793
805
  function createSourcesLock(sources) {
794
806
  const entries = sources
795
807
  .map((source) => ({
@@ -997,38 +1009,12 @@ export function iconAssets(index) {
997
1009
  }
998
1010
  return [...assets.values()].toSorted((left, right) => left.name.localeCompare(right.name));
999
1011
  }
1000
- export async function writeArtifacts(generatedDirectory, artifacts) {
1001
- const parent = dirname(generatedDirectory);
1002
- await fs.mkdir(parent, { recursive: true });
1003
- const temporary = await fs.mkdtemp(join(parent, '.catalog-'));
1004
- try {
1005
- for (const [name, content] of Object.entries(artifacts))
1006
- await fs.writeFile(join(temporary, name), content, 'utf8');
1007
- const backup = `${generatedDirectory}.previous`;
1008
- await fs.rm(backup, { recursive: true, force: true });
1009
- let movedExisting = false;
1010
- try {
1011
- await fs.rename(generatedDirectory, backup);
1012
- movedExisting = true;
1013
- }
1014
- catch (error) {
1015
- if (error.code !== 'ENOENT')
1016
- throw error;
1017
- }
1018
- try {
1019
- await fs.rename(temporary, generatedDirectory);
1020
- }
1021
- catch (error) {
1022
- if (movedExisting)
1023
- await fs.rename(backup, generatedDirectory);
1024
- throw error;
1025
- }
1026
- if (movedExisting)
1027
- await fs.rm(backup, { recursive: true, force: true });
1028
- }
1029
- finally {
1030
- await fs.rm(temporary, { recursive: true, force: true });
1031
- }
1012
+ /**
1013
+ * Publishes a verified artifact set to `generatedDirectory` under the output's
1014
+ * coordination lock. See `catalog-store.ts` for the publication contract.
1015
+ */
1016
+ export async function writeArtifacts(generatedDirectory, artifacts, options = {}) {
1017
+ await publishCatalog(generatedDirectory, artifacts, options);
1032
1018
  }
1033
1019
  export function verifyArtifacts(artifacts) {
1034
1020
  const parsedCatalog = CatalogSchema.parse(JSON.parse(artifacts['catalog.v1.json']));
@@ -1330,10 +1316,15 @@ function createChangeReport(catalog, lock, existingLock, existingCatalog, skippe
1330
1316
  ? lock.sources
1331
1317
  .filter((source) => (() => {
1332
1318
  const previous = existingLock.sources.find((candidate) => candidate.sourceId === source.sourceId);
1319
+ const priorCatalog = existingCatalog?.sources.find((entry) => entry.sourceId === source.sourceId);
1320
+ const currentCatalog = catalog.sources.find((entry) => entry.sourceId === source.sourceId);
1333
1321
  return (!previous ||
1334
- previous?.resolvedCommitSha !== source.resolvedCommitSha ||
1335
- previous.sourceManifestDigest !== source.sourceManifestDigest ||
1336
- digest(previous.pluginPins) !== digest(source.pluginPins));
1322
+ sourceLockChanged(previous, source) ||
1323
+ (existingCatalog !== undefined &&
1324
+ (!priorCatalog ||
1325
+ !currentCatalog ||
1326
+ priorCatalog.displayName !== currentCatalog.displayName ||
1327
+ priorCatalog.trustClassification !== currentCatalog.trustClassification)));
1337
1328
  })())
1338
1329
  .map((source) => source.sourceId)
1339
1330
  : lock.sources.map((source) => source.sourceId);
@@ -1416,7 +1407,10 @@ async function resolvePluginPins(input) {
1416
1407
  }
1417
1408
  const repositoryUrl = canonicalRepositoryUrl(entry.source.repositoryUrl);
1418
1409
  const pluginSubdirectory = entry.source.subdirectory;
1419
- const locked = input.lockedPins?.find((pin) => pin.pluginName === entry.name);
1410
+ // Existing locks describe the prior sync; only explicit replay freezes revisions.
1411
+ const locked = input.fromLock
1412
+ ? input.lockedPins?.find((pin) => pin.pluginName === entry.name)
1413
+ : undefined;
1420
1414
  if (input.fromLock && !locked)
1421
1415
  throw new Error(`SOURCE_LOCK_PLUGIN_PIN_MISSING: ${input.config.sourceId}:${entry.name}`);
1422
1416
  if (locked) {
@@ -1428,13 +1422,16 @@ async function resolvePluginPins(input) {
1428
1422
  }
1429
1423
  const resolvedCommitSha = entry.source.sha ??
1430
1424
  (input.resolveExternalRefs
1431
- ? await resolveGitRef(repositoryUrl, entry.source.ref?.startsWith('refs/')
1432
- ? entry.source.ref
1433
- : `refs/heads/${entry.source.ref ?? 'main'}`)
1425
+ ? await resolveGitRef(repositoryUrl, externalGitRef(entry.source.ref))
1434
1426
  : input.sourceCommitSha);
1435
1427
  return { pluginName: entry.name, repositoryUrl, pluginSubdirectory, resolvedCommitSha };
1436
1428
  });
1437
- return pins.toSorted((left, right) => left.pluginName.localeCompare(right.pluginName));
1429
+ return pins.toSorted((left, right) => left.pluginName < right.pluginName ? -1 : left.pluginName > right.pluginName ? 1 : 0);
1430
+ }
1431
+ function externalGitRef(ref) {
1432
+ if (ref === undefined)
1433
+ return 'HEAD';
1434
+ return ref.startsWith('refs/') ? ref : `refs/heads/${ref}`;
1438
1435
  }
1439
1436
  async function mapWithConcurrency(values, concurrency, mapper) {
1440
1437
  const results = Array.from({ length: values.length });
@@ -1456,7 +1453,8 @@ async function readFixtureManifest(config, fixtureRoot) {
1456
1453
  return fs.readFile(join(resolve(fixtureRoot), config.fixturePath, config.manifestPath), 'utf8');
1457
1454
  }
1458
1455
  async function resolveGitRef(repositoryUrl, ref) {
1459
- const result = spawn('git', ['ls-remote', canonicalRepositoryUrl(repositoryUrl), ref], {
1456
+ const peeled = ref.startsWith('refs/tags/') && !ref.endsWith('^{}') ? `${ref}^{}` : undefined;
1457
+ const result = spawn('git', ['ls-remote', canonicalRepositoryUrl(repositoryUrl), ref, ...(peeled ? [peeled] : [])], {
1460
1458
  stdio: ['ignore', 'pipe', 'pipe'],
1461
1459
  });
1462
1460
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -1479,7 +1477,12 @@ async function resolveGitRef(repositoryUrl, ref) {
1479
1477
  ]);
1480
1478
  if (exitCode !== 0)
1481
1479
  throw new Error(`GIT_REF_LOOKUP_FAILED: ${repositoryUrl}:${ref}:${stderr.trim()}`);
1482
- const sha = stdout.trim().split(/\s+/)[0];
1480
+ const rows = stdout
1481
+ .trim()
1482
+ .split('\n')
1483
+ .map((line) => line.split(/\s+/));
1484
+ const sha = (peeled && rows.find((row) => row[1] === peeled)?.[0]) ??
1485
+ rows.find((row) => row[1] === ref)?.[0];
1483
1486
  if (!sha || !/^[a-f0-9]{40}$/.test(sha))
1484
1487
  throw new Error(`GIT_REF_NOT_FOUND: ${repositoryUrl}:${ref}`);
1485
1488
  return sha;
@@ -1503,7 +1506,8 @@ function rawUrl(repositoryUrl, commitSha, path) {
1503
1506
  return `https://raw.githubusercontent.com${parsed.pathname}/${commitSha}/${path.split('/').map(encodeURIComponent).join('/')}`;
1504
1507
  }
1505
1508
  async function fetchText(url) {
1506
- const response = await fetch(url, { headers: upstreamRequestHeaders(url) });
1509
+ // Manifest requests use the same bounded retry policy as the tree and archive.
1510
+ const response = await fetchUpstream(url, { headers: upstreamRequestHeaders(url) });
1507
1511
  if (!response.ok)
1508
1512
  throw new Error(`HTTP_FETCH_FAILED: ${response.status}:${url}`);
1509
1513
  return response.text();
@@ -1579,9 +1583,6 @@ const UPSTREAM_WEATHER_SKIP_CODES = new Set([
1579
1583
  'PLUGIN_FILE_UNAVAILABLE',
1580
1584
  'PLUGIN_PATH_NOT_FOUND',
1581
1585
  ]);
1582
- function sleep(ms) {
1583
- return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
1584
- }
1585
1586
  function upstreamRetryDelayMs(attempt, response) {
1586
1587
  const retryAfter = response?.headers.get('retry-after');
1587
1588
  if (retryAfter !== null && retryAfter !== undefined) {
@@ -1615,9 +1616,9 @@ async function fetchUpstream(url, init) {
1615
1616
  response = await fetch(url, init);
1616
1617
  }
1617
1618
  catch (error) {
1618
- if (attempt >= UPSTREAM_FETCH_MAX_ATTEMPTS - 1)
1619
+ if (init.signal?.aborted || attempt >= UPSTREAM_FETCH_MAX_ATTEMPTS - 1)
1619
1620
  throw error;
1620
- await sleep(upstreamRetryDelayMs(attempt));
1621
+ await sleep(upstreamRetryDelayMs(attempt), undefined, { signal: init.signal ?? undefined });
1621
1622
  attempt += 1;
1622
1623
  continue;
1623
1624
  }
@@ -1625,7 +1626,10 @@ async function fetchUpstream(url, init) {
1625
1626
  !isRetryableUpstreamStatus(response.status, response) ||
1626
1627
  attempt >= UPSTREAM_FETCH_MAX_ATTEMPTS - 1)
1627
1628
  return response;
1628
- await sleep(upstreamRetryDelayMs(attempt, response));
1629
+ await response.body?.cancel().catch(() => { });
1630
+ await sleep(upstreamRetryDelayMs(attempt, response), undefined, {
1631
+ signal: init.signal ?? undefined,
1632
+ });
1629
1633
  attempt += 1;
1630
1634
  }
1631
1635
  }
@@ -1740,23 +1744,29 @@ export class NetworkSnapshotLoader {
1740
1744
  if (!owner || !repo)
1741
1745
  throw new Error('GIT_REPOSITORY_INVALID');
1742
1746
  const key = `${owner}/${repo}@${commitSha}`;
1743
- let tree = this.#trees.get(key);
1744
- if (!tree) {
1745
- const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${commitSha}?recursive=1`;
1746
- const response = await fetchUpstream(treeUrl, {
1747
- headers: upstreamRequestHeaders(treeUrl),
1747
+ let pendingTree = this.#trees.get(key);
1748
+ if (!pendingTree) {
1749
+ pendingTree = (async () => {
1750
+ const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${commitSha}?recursive=1`;
1751
+ const response = await fetchUpstream(treeUrl, {
1752
+ headers: upstreamRequestHeaders(treeUrl),
1753
+ });
1754
+ if (!response.ok)
1755
+ throw new PluginSafetyError('PLUGIN_SOURCE_UNAVAILABLE', [`${owner}/${repo}`]);
1756
+ const payload = (await response.json());
1757
+ if (payload.truncated)
1758
+ throw new Error(`GIT_TREE_TRUNCATED: ${owner}/${repo}@${commitSha}`);
1759
+ return (payload.tree ?? []).map((entry) => ({
1760
+ ...entry,
1761
+ path: safeRelativePath(entry.path, 'TREE_PATH_INVALID'),
1762
+ }));
1763
+ })().catch((error) => {
1764
+ this.#trees.delete(key);
1765
+ throw error;
1748
1766
  });
1749
- if (!response.ok)
1750
- throw new PluginSafetyError('PLUGIN_SOURCE_UNAVAILABLE', [`${owner}/${repo}`]);
1751
- const payload = (await response.json());
1752
- if (payload.truncated)
1753
- throw new Error(`GIT_TREE_TRUNCATED: ${owner}/${repo}@${commitSha}`);
1754
- tree = (payload.tree ?? []).map((entry) => ({
1755
- ...entry,
1756
- path: safeRelativePath(entry.path, 'TREE_PATH_INVALID'),
1757
- }));
1758
- this.#trees.set(key, tree);
1767
+ this.#trees.set(key, pendingTree);
1759
1768
  }
1769
+ const tree = await pendingTree;
1760
1770
  const prefix = pluginSubdirectory ? `${safeRelativePath(pluginSubdirectory)}/` : '';
1761
1771
  const submodules = tree.filter((entry) => entry.path.startsWith(prefix) && (entry.type === 'commit' || entry.mode === '160000'));
1762
1772
  if (submodules.length > 0)
@@ -1799,13 +1809,18 @@ export class NetworkSnapshotLoader {
1799
1809
  const cached = this.#archives.get(key);
1800
1810
  if (cached)
1801
1811
  return cached;
1802
- const url = `https://api.github.com/repos/${owner}/${repo}/tarball/${commitSha}`;
1803
- const response = await fetchUpstream(url, { headers: upstreamRequestHeaders(url) });
1804
- if (!response.ok)
1805
- throw new PluginSafetyError('PLUGIN_SOURCE_UNAVAILABLE', [`${owner}/${repo}`]);
1806
- const files = await readArchiveFiles(new Uint8Array(await response.arrayBuffer()), this.#policy);
1807
- this.#archives.set(key, files);
1808
- return files;
1812
+ const pending = (async () => {
1813
+ const url = `https://api.github.com/repos/${owner}/${repo}/tarball/${commitSha}`;
1814
+ const response = await fetchUpstream(url, { headers: upstreamRequestHeaders(url) });
1815
+ if (!response.ok)
1816
+ throw new PluginSafetyError('PLUGIN_SOURCE_UNAVAILABLE', [`${owner}/${repo}`]);
1817
+ return readArchiveFiles(new Uint8Array(await response.arrayBuffer()), this.#policy);
1818
+ })().catch((error) => {
1819
+ this.#archives.delete(key);
1820
+ throw error;
1821
+ });
1822
+ this.#archives.set(key, pending);
1823
+ return pending;
1809
1824
  }
1810
1825
  }
1811
1826
  export class FixtureSnapshotLoader {