runtime-resolver 0.3.12 → 0.3.14
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/bin/runtime-resolver.d.ts +1 -0
- package/bin/runtime-resolver.js +29 -261
- package/cli/commands/resolve.js +239 -0
- package/cli/schemas/json-schema.js +12 -0
- package/cli/schemas/response.js +86 -0
- package/data/bun-defaults.js +850 -0
- package/data/deno-defaults.js +1526 -0
- package/data/node-defaults.js +4400 -0
- package/errors/AuthenticationError.js +35 -0
- package/errors/CacheError.js +32 -0
- package/errors/FreshnessError.js +38 -0
- package/errors/InvalidInputError.js +33 -0
- package/errors/NetworkError.js +34 -0
- package/errors/ParseError.js +33 -0
- package/errors/RateLimitError.js +39 -0
- package/errors/VersionNotFoundError.js +37 -0
- package/index.d.ts +2577 -2608
- package/index.js +158 -132
- package/layers/AutoBunCacheLive.js +47 -0
- package/layers/AutoDenoCacheLive.js +47 -0
- package/layers/AutoNodeCacheLive.js +58 -0
- package/layers/BunReleaseCacheLive.js +25 -0
- package/layers/BunResolverLive.js +85 -0
- package/layers/BunVersionFetcherLive.js +55 -0
- package/layers/DenoReleaseCacheLive.js +25 -0
- package/layers/DenoResolverLive.js +84 -0
- package/layers/DenoVersionFetcherLive.js +51 -0
- package/layers/FreshBunCacheLive.js +60 -0
- package/layers/FreshDenoCacheLive.js +60 -0
- package/layers/FreshNodeCacheLive.js +55 -0
- package/layers/GitHubAppAuth.js +58 -0
- package/layers/GitHubAutoAuth.js +70 -0
- package/layers/GitHubClientLive.js +130 -0
- package/layers/GitHubTokenAuth.js +50 -0
- package/layers/NodeReleaseCacheLive.js +77 -0
- package/layers/NodeResolverLive.js +107 -0
- package/layers/NodeScheduleFetcherLive.js +37 -0
- package/layers/NodeVersionFetcherLive.js +55 -0
- package/layers/OfflineBunCacheLive.js +40 -0
- package/layers/OfflineDenoCacheLive.js +40 -0
- package/layers/OfflineNodeCacheLive.js +39 -0
- package/layers/RuntimeCacheLive.js +78 -0
- package/lib/retry.js +27 -0
- package/lib/semver.js +21 -0
- package/package.json +57 -68
- package/schemas/bun-release.js +61 -0
- package/schemas/deno-release.js +61 -0
- package/schemas/github.js +74 -0
- package/schemas/node-release.js +130 -0
- package/schemas/node-schedule.js +110 -0
- package/schemas/node.js +68 -0
- package/services/BunReleaseCache.js +33 -0
- package/services/BunResolver.js +36 -0
- package/services/BunVersionFetcher.js +41 -0
- package/services/DenoReleaseCache.js +33 -0
- package/services/DenoResolver.js +36 -0
- package/services/DenoVersionFetcher.js +41 -0
- package/services/GitHubClient.js +40 -0
- package/services/NodeReleaseCache.js +41 -0
- package/services/NodeResolver.js +44 -0
- package/services/NodeScheduleFetcher.js +38 -0
- package/services/NodeVersionFetcher.js +39 -0
- package/services/OctokitInstance.js +44 -0
- package/tsdoc-metadata.json +11 -11
- package/612.js +0 -23
- package/711.js +0 -7554
- package/json-schema.js +0 -95
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createRuntimeCache } from "./RuntimeCacheLive.js";
|
|
2
|
+
import { NodeReleaseCache } from "../services/NodeReleaseCache.js";
|
|
3
|
+
import { NodeRelease } from "../schemas/node-release.js";
|
|
4
|
+
import { NodeSchedule } from "../schemas/node-schedule.js";
|
|
5
|
+
import { Effect, Layer, Ref } from "effect";
|
|
6
|
+
|
|
7
|
+
//#region src/layers/NodeReleaseCacheLive.ts
|
|
8
|
+
/**
|
|
9
|
+
* Low-level layer that constructs the {@link NodeReleaseCache} service backed by
|
|
10
|
+
* a `SemVerVersionCache` instance from `semver-effect`.
|
|
11
|
+
*
|
|
12
|
+
* This layer is more complex than the Bun and Deno equivalents because it manages
|
|
13
|
+
* additional LTS schedule state via an internal `Ref`. The schedule is stored
|
|
14
|
+
* separately from the version list so that calling `updateSchedule` can
|
|
15
|
+
* transparently rebuild all release objects with the latest phase information
|
|
16
|
+
* without reloading data from the network.
|
|
17
|
+
*
|
|
18
|
+
* This layer wires up the cache storage but does not populate it with any
|
|
19
|
+
* release data. Callers are responsible for calling `updateSchedule` and
|
|
20
|
+
* `loadFromInputs` after construction. In normal usage you should prefer the
|
|
21
|
+
* higher-level cache strategy layers instead of using this layer directly:
|
|
22
|
+
* - {@link AutoNodeCacheLive} — API with fallback to bundled defaults
|
|
23
|
+
* - {@link FreshNodeCacheLive} — API only, fails if unavailable
|
|
24
|
+
* - {@link OfflineNodeCacheLive} — bundled defaults only
|
|
25
|
+
*
|
|
26
|
+
* @see {@link NodeReleaseCache}
|
|
27
|
+
* @see {@link createRuntimeCache}
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
const NodeReleaseCacheLive = Layer.effect(NodeReleaseCache, Effect.gen(function* () {
|
|
31
|
+
const scheduleRef = yield* Ref.make(NodeSchedule.fromData({}));
|
|
32
|
+
const inner = yield* createRuntimeCache();
|
|
33
|
+
let currentInputs = [];
|
|
34
|
+
const buildAndLoad = (inputs) => Effect.gen(function* () {
|
|
35
|
+
const releases = [];
|
|
36
|
+
for (const input of inputs) {
|
|
37
|
+
const release = yield* NodeRelease.fromInput(input, scheduleRef).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
38
|
+
if (release) releases.push(release);
|
|
39
|
+
}
|
|
40
|
+
yield* inner.load(releases);
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
load: (releases) => inner.load(releases),
|
|
44
|
+
resolve: (range) => inner.resolve(range),
|
|
45
|
+
releases: () => inner.releases(),
|
|
46
|
+
filter: (range) => inner.filter(range),
|
|
47
|
+
latest: () => inner.latest(),
|
|
48
|
+
latestByMajor: () => inner.latestByMajor(),
|
|
49
|
+
latestByMinor: () => inner.latestByMinor(),
|
|
50
|
+
updateSchedule: (data) => Effect.gen(function* () {
|
|
51
|
+
yield* Ref.set(scheduleRef, NodeSchedule.fromData(data));
|
|
52
|
+
if (currentInputs.length > 0) yield* buildAndLoad(currentInputs);
|
|
53
|
+
}),
|
|
54
|
+
loadFromInputs: (inputs) => Effect.gen(function* () {
|
|
55
|
+
currentInputs = inputs;
|
|
56
|
+
yield* buildAndLoad(inputs);
|
|
57
|
+
}),
|
|
58
|
+
ltsReleases: (now) => Effect.gen(function* () {
|
|
59
|
+
const all = yield* inner.releases();
|
|
60
|
+
const results = [];
|
|
61
|
+
for (const r of all) {
|
|
62
|
+
const phase = yield* r.phase(now);
|
|
63
|
+
if (phase === "active-lts" || phase === "maintenance-lts") results.push(r);
|
|
64
|
+
}
|
|
65
|
+
return results;
|
|
66
|
+
}),
|
|
67
|
+
currentReleases: (now) => Effect.gen(function* () {
|
|
68
|
+
const all = yield* inner.releases();
|
|
69
|
+
const results = [];
|
|
70
|
+
for (const r of all) if ((yield* r.phase(now)) === "current") results.push(r);
|
|
71
|
+
return results;
|
|
72
|
+
})
|
|
73
|
+
};
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { NodeReleaseCacheLive };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { NodeReleaseCache } from "../services/NodeReleaseCache.js";
|
|
2
|
+
import { VersionNotFoundError } from "../errors/VersionNotFoundError.js";
|
|
3
|
+
import { NodeResolver } from "../services/NodeResolver.js";
|
|
4
|
+
import { DateTime, Effect, Layer } from "effect";
|
|
5
|
+
import { SemVer } from "semver-effect";
|
|
6
|
+
|
|
7
|
+
//#region src/layers/NodeResolverLive.ts
|
|
8
|
+
/**
|
|
9
|
+
* Provides the {@link NodeResolver} service backed by a {@link NodeReleaseCache}.
|
|
10
|
+
*
|
|
11
|
+
* This layer composes with any of the three Node cache strategy layers to form
|
|
12
|
+
* a complete resolver stack. In addition to semver range filtering and increment
|
|
13
|
+
* grouping, it supports filtering by Node.js release phase (e.g., `"current"`,
|
|
14
|
+
* `"active-lts"`, `"maintenance-lts"`, `"end-of-life"`) and automatically
|
|
15
|
+
* determines the current LTS version from the loaded schedule data.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import {
|
|
20
|
+
* NodeResolverLive, AutoNodeCacheLive,
|
|
21
|
+
* NodeVersionFetcherLive, NodeScheduleFetcherLive,
|
|
22
|
+
* GitHubClientLive, GitHubAutoAuth,
|
|
23
|
+
* } from "runtime-resolver";
|
|
24
|
+
* import { NodeResolver } from "runtime-resolver";
|
|
25
|
+
* import { Effect, Layer } from "effect";
|
|
26
|
+
*
|
|
27
|
+
* const GitHubLayer = GitHubClientLive.pipe(Layer.provide(GitHubAutoAuth));
|
|
28
|
+
* const FetchersLayer = Layer.merge(
|
|
29
|
+
* NodeVersionFetcherLive.pipe(Layer.provide(GitHubLayer)),
|
|
30
|
+
* NodeScheduleFetcherLive.pipe(Layer.provide(GitHubLayer)),
|
|
31
|
+
* );
|
|
32
|
+
* const CacheLayer = AutoNodeCacheLive.pipe(Layer.provide(FetchersLayer));
|
|
33
|
+
* const ResolverLayer = NodeResolverLive.pipe(Layer.provide(CacheLayer));
|
|
34
|
+
*
|
|
35
|
+
* const program = Effect.gen(function* () {
|
|
36
|
+
* const resolver = yield* NodeResolver;
|
|
37
|
+
* // Resolve only active-LTS and maintenance-LTS releases in the ^20 range
|
|
38
|
+
* const result = yield* resolver.resolve({
|
|
39
|
+
* semverRange: "^20.0.0",
|
|
40
|
+
* phases: ["active-lts", "maintenance-lts"],
|
|
41
|
+
* });
|
|
42
|
+
* console.log(result.lts);
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* Effect.runPromise(program.pipe(Effect.provide(ResolverLayer)));
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @see {@link NodeResolver}
|
|
49
|
+
* @see {@link NodeReleaseCache}
|
|
50
|
+
* @see {@link AutoNodeCacheLive}
|
|
51
|
+
* @see {@link FreshNodeCacheLive}
|
|
52
|
+
* @see {@link OfflineNodeCacheLive}
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
const NodeResolverLive = Layer.effect(NodeResolver, Effect.gen(function* () {
|
|
56
|
+
const cache = yield* NodeReleaseCache;
|
|
57
|
+
return { resolve: (options) => Effect.gen(function* () {
|
|
58
|
+
const semverRange = options?.semverRange ?? "*";
|
|
59
|
+
const phases = options?.phases ?? ["current", "active-lts"];
|
|
60
|
+
const increments = options?.increments ?? "latest";
|
|
61
|
+
const now = options?.date ? DateTime.unsafeMake(options.date) : DateTime.unsafeMake(/* @__PURE__ */ new Date());
|
|
62
|
+
const matching = yield* cache.filter(semverRange).pipe(Effect.catchAll(() => Effect.succeed([])));
|
|
63
|
+
const phaseFiltered = [];
|
|
64
|
+
for (const r of matching) {
|
|
65
|
+
const phase = yield* r.phase(now);
|
|
66
|
+
if (phase && phases.includes(phase)) phaseFiltered.push(r);
|
|
67
|
+
}
|
|
68
|
+
let resultReleases;
|
|
69
|
+
if (increments === "latest") {
|
|
70
|
+
const groups = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const r of phaseFiltered) {
|
|
72
|
+
const existing = groups.get(r.version.major);
|
|
73
|
+
if (!existing || SemVer.gt(r.version, existing.version)) groups.set(r.version.major, r);
|
|
74
|
+
}
|
|
75
|
+
resultReleases = [...groups.values()];
|
|
76
|
+
} else if (increments === "minor") {
|
|
77
|
+
const groups = /* @__PURE__ */ new Map();
|
|
78
|
+
for (const r of phaseFiltered) {
|
|
79
|
+
const key = `${r.version.major}.${r.version.minor}`;
|
|
80
|
+
const existing = groups.get(key);
|
|
81
|
+
if (!existing || SemVer.gt(r.version, existing.version)) groups.set(key, r);
|
|
82
|
+
}
|
|
83
|
+
resultReleases = [...groups.values()];
|
|
84
|
+
} else resultReleases = phaseFiltered;
|
|
85
|
+
if (resultReleases.length === 0) return yield* Effect.fail(new VersionNotFoundError({
|
|
86
|
+
runtime: "node",
|
|
87
|
+
constraint: semverRange,
|
|
88
|
+
message: `No Node.js versions found matching "${semverRange}" with phases [${phases.join(", ")}]`
|
|
89
|
+
}));
|
|
90
|
+
const versions = SemVer.rsort(resultReleases.map((r) => r.version)).map((v) => resultReleases.find((r) => SemVer.equal(r.version, v))).filter((r) => r !== void 0).map((r) => r.version.toString());
|
|
91
|
+
const latest = versions[0];
|
|
92
|
+
const ltsVersions = (yield* cache.ltsReleases(now)).filter((r) => versions.includes(r.version.toString())).map((r) => r.version);
|
|
93
|
+
const lts = ltsVersions.length > 0 ? SemVer.rsort(ltsVersions)[0].toString() : void 0;
|
|
94
|
+
let resolvedDefault;
|
|
95
|
+
if (options?.defaultVersion) resolvedDefault = yield* cache.resolve(options.defaultVersion).pipe(Effect.map((r) => r.version.toString()), Effect.catchAll(() => Effect.succeed(void 0)));
|
|
96
|
+
return {
|
|
97
|
+
source: "api",
|
|
98
|
+
versions,
|
|
99
|
+
latest,
|
|
100
|
+
...lts ? { lts } : {},
|
|
101
|
+
...resolvedDefault ? { default: resolvedDefault } : { ...lts ? { default: lts } : {} }
|
|
102
|
+
};
|
|
103
|
+
}) };
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
//#endregion
|
|
107
|
+
export { NodeResolverLive };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NodeScheduleFetcher } from "../services/NodeScheduleFetcher.js";
|
|
2
|
+
import { GitHubClient } from "../services/GitHubClient.js";
|
|
3
|
+
import { ParseError } from "../errors/ParseError.js";
|
|
4
|
+
import { NodeReleaseSchedule } from "../schemas/node.js";
|
|
5
|
+
import { Effect, Layer, Schema } from "effect";
|
|
6
|
+
|
|
7
|
+
//#region src/layers/NodeScheduleFetcherLive.ts
|
|
8
|
+
const NODE_SCHEDULE_URL = "https://raw.githubusercontent.com/nodejs/Release/refs/heads/main/schedule.json";
|
|
9
|
+
const decodeSchedule = (input) => Schema.decodeUnknown(NodeReleaseSchedule)(input).pipe(Effect.mapError((e) => new ParseError({
|
|
10
|
+
source: NODE_SCHEDULE_URL,
|
|
11
|
+
message: `Schema validation failed: ${e.message}`
|
|
12
|
+
})), Effect.map((data) => data));
|
|
13
|
+
/**
|
|
14
|
+
* Provides the {@link NodeScheduleFetcher} service using the live Node.js release schedule.
|
|
15
|
+
*
|
|
16
|
+
* Fetches the Node.js release schedule from the raw GitHub content URL for
|
|
17
|
+
* `nodejs/Release` (`schedule.json` on the `main` branch) using the HTTP client
|
|
18
|
+
* on the provided {@link GitHubClient} service. The schedule data maps major
|
|
19
|
+
* version codenames to their start, LTS, and end-of-life dates and is used by
|
|
20
|
+
* {@link NodeReleaseCache} to compute per-release lifecycle phases.
|
|
21
|
+
*
|
|
22
|
+
* This layer is a required dependency of {@link AutoNodeCacheLive} and
|
|
23
|
+
* {@link FreshNodeCacheLive}. It is not needed by {@link OfflineNodeCacheLive}.
|
|
24
|
+
*
|
|
25
|
+
* @see {@link NodeScheduleFetcher}
|
|
26
|
+
* @see {@link GitHubClient}
|
|
27
|
+
* @see {@link AutoNodeCacheLive}
|
|
28
|
+
* @see {@link FreshNodeCacheLive}
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
const NodeScheduleFetcherLive = Layer.effect(NodeScheduleFetcher, Effect.gen(function* () {
|
|
32
|
+
const client = yield* GitHubClient;
|
|
33
|
+
return { fetch: () => client.getJson(NODE_SCHEDULE_URL, { decode: decodeSchedule }) };
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { NodeScheduleFetcherLive };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NodeVersionFetcher } from "../services/NodeVersionFetcher.js";
|
|
2
|
+
import { tryParseSemVer } from "../lib/semver.js";
|
|
3
|
+
import { GitHubClient } from "../services/GitHubClient.js";
|
|
4
|
+
import { ParseError } from "../errors/ParseError.js";
|
|
5
|
+
import { NodeDistIndex } from "../schemas/node.js";
|
|
6
|
+
import { Effect, Layer, Option, Schema } from "effect";
|
|
7
|
+
|
|
8
|
+
//#region src/layers/NodeVersionFetcherLive.ts
|
|
9
|
+
const NODE_DIST_URL = "https://nodejs.org/dist/index.json";
|
|
10
|
+
const decodeDistIndex = (input) => Schema.decodeUnknown(NodeDistIndex)(input).pipe(Effect.mapError((e) => new ParseError({
|
|
11
|
+
source: NODE_DIST_URL,
|
|
12
|
+
message: `Schema validation failed: ${e.message}`
|
|
13
|
+
})));
|
|
14
|
+
/**
|
|
15
|
+
* Provides the {@link NodeVersionFetcher} service using the live Node.js dist index.
|
|
16
|
+
*
|
|
17
|
+
* Fetches the full Node.js version list from `https://nodejs.org/dist/index.json`
|
|
18
|
+
* using the HTTP client on the provided {@link GitHubClient} service. Version
|
|
19
|
+
* strings are normalized by stripping the leading `v` before parsing as semver.
|
|
20
|
+
* Entries with non-string `npm` fields (present in very old releases prior to
|
|
21
|
+
* v0.6.3) are substituted with the sentinel version `"0.0.0"`.
|
|
22
|
+
*
|
|
23
|
+
* This layer is a required dependency of {@link AutoNodeCacheLive} and
|
|
24
|
+
* {@link FreshNodeCacheLive}. It is not needed by {@link OfflineNodeCacheLive}.
|
|
25
|
+
*
|
|
26
|
+
* @see {@link NodeVersionFetcher}
|
|
27
|
+
* @see {@link GitHubClient}
|
|
28
|
+
* @see {@link AutoNodeCacheLive}
|
|
29
|
+
* @see {@link FreshNodeCacheLive}
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
const NodeVersionFetcherLive = Layer.effect(NodeVersionFetcher, Effect.gen(function* () {
|
|
33
|
+
const client = yield* GitHubClient;
|
|
34
|
+
return { fetch: () => Effect.gen(function* () {
|
|
35
|
+
const allVersions = yield* client.getJson(NODE_DIST_URL, { decode: decodeDistIndex });
|
|
36
|
+
const parsed = yield* Effect.forEach(allVersions, (entry) => {
|
|
37
|
+
const clean = entry.version.replace(/^v/, "");
|
|
38
|
+
return tryParseSemVer(clean).pipe(Effect.map((opt) => Option.isSome(opt) ? Option.some({
|
|
39
|
+
version: opt.value,
|
|
40
|
+
input: {
|
|
41
|
+
version: clean,
|
|
42
|
+
npm: typeof entry.npm === "string" ? entry.npm : "0.0.0",
|
|
43
|
+
date: entry.date
|
|
44
|
+
}
|
|
45
|
+
}) : Option.none()));
|
|
46
|
+
}, { concurrency: "unbounded" });
|
|
47
|
+
return {
|
|
48
|
+
versions: parsed.flatMap(Option.toArray).map(({ version }) => version),
|
|
49
|
+
inputs: parsed.flatMap(Option.toArray).map(({ input }) => input)
|
|
50
|
+
};
|
|
51
|
+
}) };
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { NodeVersionFetcherLive };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { bunDefaultInputs } from "../data/bun-defaults.js";
|
|
2
|
+
import { BunRelease } from "../schemas/bun-release.js";
|
|
3
|
+
import { BunReleaseCache } from "../services/BunReleaseCache.js";
|
|
4
|
+
import { BunReleaseCacheLive } from "./BunReleaseCacheLive.js";
|
|
5
|
+
import { Effect, Layer } from "effect";
|
|
6
|
+
import { SemVerParserLive, VersionCacheLive } from "semver-effect";
|
|
7
|
+
|
|
8
|
+
//#region src/layers/OfflineBunCacheLive.ts
|
|
9
|
+
const offlineSetup = Effect.gen(function* () {
|
|
10
|
+
const cache = yield* BunReleaseCache;
|
|
11
|
+
const releases = yield* Effect.all(bunDefaultInputs.map((input) => BunRelease.fromInput(input)), { concurrency: "unbounded" }).pipe(Effect.orElseSucceed(() => []));
|
|
12
|
+
yield* cache.load(releases);
|
|
13
|
+
});
|
|
14
|
+
const SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
|
|
15
|
+
const BaseCacheLayer = BunReleaseCacheLive.pipe(Layer.provide(SemVerLayer));
|
|
16
|
+
/**
|
|
17
|
+
* Provides the {@link BunReleaseCache} service using the "Offline" cache strategy.
|
|
18
|
+
*
|
|
19
|
+
* Uses only the bundled default Bun release data that was baked in at build time.
|
|
20
|
+
* Makes no network requests and has no runtime dependencies. This layer always
|
|
21
|
+
* succeeds immediately.
|
|
22
|
+
*
|
|
23
|
+
* Use this strategy in environments where network access is unavailable or
|
|
24
|
+
* undesirable (e.g., air-gapped systems, hermetic test environments, or any
|
|
25
|
+
* context where version data from the bundle is sufficient).
|
|
26
|
+
*
|
|
27
|
+
* The three Bun cache strategy layers are:
|
|
28
|
+
* - {@link AutoBunCacheLive} — tries API first, falls back to bundled defaults on any error
|
|
29
|
+
* - {@link FreshBunCacheLive} — requires live API data, fails with `FreshnessError` if unavailable
|
|
30
|
+
* - `OfflineBunCacheLive` — uses only bundled defaults, no network I/O
|
|
31
|
+
*
|
|
32
|
+
* @see {@link BunReleaseCache}
|
|
33
|
+
* @see {@link AutoBunCacheLive}
|
|
34
|
+
* @see {@link FreshBunCacheLive}
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
const OfflineBunCacheLive = BaseCacheLayer.pipe(Layer.tap((ctx) => offlineSetup.pipe(Effect.provide(ctx))));
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { OfflineBunCacheLive };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { denoDefaultInputs } from "../data/deno-defaults.js";
|
|
2
|
+
import { DenoRelease } from "../schemas/deno-release.js";
|
|
3
|
+
import { DenoReleaseCache } from "../services/DenoReleaseCache.js";
|
|
4
|
+
import { DenoReleaseCacheLive } from "./DenoReleaseCacheLive.js";
|
|
5
|
+
import { Effect, Layer } from "effect";
|
|
6
|
+
import { SemVerParserLive, VersionCacheLive } from "semver-effect";
|
|
7
|
+
|
|
8
|
+
//#region src/layers/OfflineDenoCacheLive.ts
|
|
9
|
+
const offlineSetup = Effect.gen(function* () {
|
|
10
|
+
const cache = yield* DenoReleaseCache;
|
|
11
|
+
const releases = yield* Effect.all(denoDefaultInputs.map((input) => DenoRelease.fromInput(input)), { concurrency: "unbounded" }).pipe(Effect.orElseSucceed(() => []));
|
|
12
|
+
yield* cache.load(releases);
|
|
13
|
+
});
|
|
14
|
+
const SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
|
|
15
|
+
const BaseCacheLayer = DenoReleaseCacheLive.pipe(Layer.provide(SemVerLayer));
|
|
16
|
+
/**
|
|
17
|
+
* Provides the {@link DenoReleaseCache} service using the "Offline" cache strategy.
|
|
18
|
+
*
|
|
19
|
+
* Uses only the bundled default Deno release data that was baked in at build time.
|
|
20
|
+
* Makes no network requests and has no runtime dependencies. This layer always
|
|
21
|
+
* succeeds immediately.
|
|
22
|
+
*
|
|
23
|
+
* Use this strategy in environments where network access is unavailable or
|
|
24
|
+
* undesirable (e.g., air-gapped systems, hermetic test environments, or any
|
|
25
|
+
* context where version data from the bundle is sufficient).
|
|
26
|
+
*
|
|
27
|
+
* The three Deno cache strategy layers are:
|
|
28
|
+
* - {@link AutoDenoCacheLive} — tries API first, falls back to bundled defaults on any error
|
|
29
|
+
* - {@link FreshDenoCacheLive} — requires live API data, fails with `FreshnessError` if unavailable
|
|
30
|
+
* - `OfflineDenoCacheLive` — uses only bundled defaults, no network I/O
|
|
31
|
+
*
|
|
32
|
+
* @see {@link DenoReleaseCache}
|
|
33
|
+
* @see {@link AutoDenoCacheLive}
|
|
34
|
+
* @see {@link FreshDenoCacheLive}
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
const OfflineDenoCacheLive = BaseCacheLayer.pipe(Layer.tap((ctx) => offlineSetup.pipe(Effect.provide(ctx))));
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { OfflineDenoCacheLive };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { nodeDefaultInputs, nodeDefaultScheduleData } from "../data/node-defaults.js";
|
|
2
|
+
import { NodeReleaseCache } from "../services/NodeReleaseCache.js";
|
|
3
|
+
import { NodeReleaseCacheLive } from "./NodeReleaseCacheLive.js";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
|
+
import { SemVerParserLive, VersionCacheLive } from "semver-effect";
|
|
6
|
+
|
|
7
|
+
//#region src/layers/OfflineNodeCacheLive.ts
|
|
8
|
+
const offlineSetup = Effect.gen(function* () {
|
|
9
|
+
const cache = yield* NodeReleaseCache;
|
|
10
|
+
yield* cache.updateSchedule(nodeDefaultScheduleData);
|
|
11
|
+
yield* cache.loadFromInputs(nodeDefaultInputs);
|
|
12
|
+
});
|
|
13
|
+
const SemVerLayer = VersionCacheLive.pipe(Layer.provide(SemVerParserLive));
|
|
14
|
+
const BaseCacheLayer = NodeReleaseCacheLive.pipe(Layer.provide(SemVerLayer));
|
|
15
|
+
/**
|
|
16
|
+
* Provides the {@link NodeReleaseCache} service using the "Offline" cache strategy.
|
|
17
|
+
*
|
|
18
|
+
* Uses only the bundled default Node.js release data and schedule data that were
|
|
19
|
+
* baked in at build time. Makes no network requests and has no runtime
|
|
20
|
+
* dependencies. This layer always succeeds immediately.
|
|
21
|
+
*
|
|
22
|
+
* Use this strategy in environments where network access is unavailable or
|
|
23
|
+
* undesirable (e.g., air-gapped systems, hermetic test environments, or any
|
|
24
|
+
* context where version data from the bundle is sufficient).
|
|
25
|
+
*
|
|
26
|
+
* The three Node cache strategy layers are:
|
|
27
|
+
* - {@link AutoNodeCacheLive} — tries API first, falls back to bundled defaults on any error
|
|
28
|
+
* - {@link FreshNodeCacheLive} — requires live API data, fails with `FreshnessError` if unavailable
|
|
29
|
+
* - `OfflineNodeCacheLive` — uses only bundled defaults, no network I/O
|
|
30
|
+
*
|
|
31
|
+
* @see {@link NodeReleaseCache}
|
|
32
|
+
* @see {@link AutoNodeCacheLive}
|
|
33
|
+
* @see {@link FreshNodeCacheLive}
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
const OfflineNodeCacheLive = BaseCacheLayer.pipe(Layer.tap((ctx) => offlineSetup.pipe(Effect.provide(ctx))));
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { OfflineNodeCacheLive };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Range, VersionCache } from "semver-effect";
|
|
3
|
+
|
|
4
|
+
//#region src/layers/RuntimeCacheLive.ts
|
|
5
|
+
/**
|
|
6
|
+
* Factory that creates a {@link RuntimeCache} implementation backed by
|
|
7
|
+
* `semver-effect`'s `VersionCache`.
|
|
8
|
+
*
|
|
9
|
+
* The returned `Effect` must be executed within an Effect context that provides
|
|
10
|
+
* a `SemVerVersionCache` service (i.e., it requires `SemVerVersionCache` in its
|
|
11
|
+
* environment). The cache maintains an internal lookup map from version string
|
|
12
|
+
* to the full release object so that resolved semver versions can be
|
|
13
|
+
* round-tripped back to their typed release records.
|
|
14
|
+
*
|
|
15
|
+
* This factory is used internally by {@link BunReleaseCacheLive},
|
|
16
|
+
* {@link DenoReleaseCacheLive}, and {@link NodeReleaseCacheLive} to build their
|
|
17
|
+
* respective cache service implementations. Prefer those higher-level layers
|
|
18
|
+
* over calling this factory directly.
|
|
19
|
+
*
|
|
20
|
+
* Note: `load` is not concurrency-safe. Callers must not invoke it concurrently.
|
|
21
|
+
* In practice this constraint is upheld because `load` is only called from layer
|
|
22
|
+
* setup and `NodeReleaseCacheLive.loadFromInputs`, both of which are serialized
|
|
23
|
+
* by the Effect runtime.
|
|
24
|
+
*
|
|
25
|
+
* @typeParam R - The runtime release type stored in the cache. Must extend
|
|
26
|
+
* {@link RuntimeRelease}.
|
|
27
|
+
*
|
|
28
|
+
* @see {@link RuntimeCache}
|
|
29
|
+
* @see {@link BunReleaseCacheLive}
|
|
30
|
+
* @see {@link DenoReleaseCacheLive}
|
|
31
|
+
* @see {@link NodeReleaseCacheLive}
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
const createRuntimeCache = () => Effect.gen(function* () {
|
|
35
|
+
const innerCache = yield* VersionCache;
|
|
36
|
+
const lookupMap = /* @__PURE__ */ new Map();
|
|
37
|
+
return {
|
|
38
|
+
load: (releases) => Effect.gen(function* () {
|
|
39
|
+
lookupMap.clear();
|
|
40
|
+
const versions = [];
|
|
41
|
+
for (const r of releases) {
|
|
42
|
+
const key = r.version.toString();
|
|
43
|
+
lookupMap.set(key, r);
|
|
44
|
+
versions.push(r.version);
|
|
45
|
+
}
|
|
46
|
+
yield* innerCache.load(versions);
|
|
47
|
+
}),
|
|
48
|
+
resolve: (range) => Effect.gen(function* () {
|
|
49
|
+
const parsed = yield* Range.parse(range);
|
|
50
|
+
const resolved = yield* innerCache.resolve(parsed);
|
|
51
|
+
const release = lookupMap.get(resolved.toString());
|
|
52
|
+
if (!release) return yield* Effect.die(/* @__PURE__ */ new Error(`Cache inconsistency: ${resolved.toString()} not in lookup map`));
|
|
53
|
+
return release;
|
|
54
|
+
}),
|
|
55
|
+
releases: () => Effect.gen(function* () {
|
|
56
|
+
return (yield* innerCache.versions.pipe(Effect.catchTag("EmptyCacheError", () => Effect.succeed([])))).map((v) => lookupMap.get(v.toString())).filter((r) => r !== void 0);
|
|
57
|
+
}),
|
|
58
|
+
filter: (range) => Effect.gen(function* () {
|
|
59
|
+
const parsed = yield* Range.parse(range);
|
|
60
|
+
return (yield* innerCache.filter(parsed).pipe(Effect.catchTag("EmptyCacheError", () => Effect.succeed([])))).map((v) => lookupMap.get(v.toString())).filter((r) => r !== void 0);
|
|
61
|
+
}),
|
|
62
|
+
latest: () => Effect.gen(function* () {
|
|
63
|
+
const version = yield* innerCache.latest();
|
|
64
|
+
const release = lookupMap.get(version.toString());
|
|
65
|
+
if (!release) return yield* Effect.die(/* @__PURE__ */ new Error(`Cache inconsistency: ${version.toString()} not in lookup map`));
|
|
66
|
+
return release;
|
|
67
|
+
}),
|
|
68
|
+
latestByMajor: () => Effect.gen(function* () {
|
|
69
|
+
return (yield* innerCache.latestByMajor().pipe(Effect.catchTag("EmptyCacheError", () => Effect.succeed([])))).map((v) => lookupMap.get(v.toString())).filter((r) => r !== void 0);
|
|
70
|
+
}),
|
|
71
|
+
latestByMinor: () => Effect.gen(function* () {
|
|
72
|
+
return (yield* innerCache.latestByMinor().pipe(Effect.catchTag("EmptyCacheError", () => Effect.succeed([])))).map((v) => lookupMap.get(v.toString())).filter((r) => r !== void 0);
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { createRuntimeCache };
|
package/lib/retry.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect, Predicate, Schedule } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/retry.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wraps an effect with automatic retry logic for {@link RateLimitError}.
|
|
6
|
+
*
|
|
7
|
+
* When the wrapped effect fails with a `RateLimitError` the retry schedule
|
|
8
|
+
* applies exponential backoff starting at a 1-second base delay, doubling on
|
|
9
|
+
* each attempt, for a maximum of 3 retries. Errors with any other tag are
|
|
10
|
+
* re-raised immediately without retrying.
|
|
11
|
+
*
|
|
12
|
+
* The predicate used for the `while` condition is
|
|
13
|
+
* `Predicate.isTagged("RateLimitError")`, so only errors whose `_tag` field
|
|
14
|
+
* equals `"RateLimitError"` trigger a retry; all other error types propagate
|
|
15
|
+
* unchanged.
|
|
16
|
+
*
|
|
17
|
+
* @see {@link RateLimitError}
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
const retryOnRateLimit = (effect) => effect.pipe(Effect.retry({
|
|
22
|
+
schedule: Schedule.exponential("1 second").pipe(Schedule.compose(Schedule.recurs(3))),
|
|
23
|
+
while: Predicate.isTagged("RateLimitError")
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { retryOnRateLimit };
|
package/lib/semver.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { SemVer } from "semver-effect";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/semver.ts
|
|
5
|
+
/**
|
|
6
|
+
* Attempts to parse a raw version string into a {@link SemVer}, returning
|
|
7
|
+
* `Option.none()` when the string is not a valid semver value.
|
|
8
|
+
*
|
|
9
|
+
* Unlike `SemVer.parse` — which fails the effect on an unparseable input —
|
|
10
|
+
* this helper absorbs parse failures and promotes them into the `Option` channel
|
|
11
|
+
* so that callers can handle invalid versions without short-circuiting the
|
|
12
|
+
* surrounding effect pipeline.
|
|
13
|
+
*
|
|
14
|
+
* @see {@link SemVer.parse}
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
const tryParseSemVer = (raw) => SemVer.parse(raw).pipe(Effect.map(Option.some), Effect.catchAll(() => Effect.succeed(Option.none())));
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { tryParseSemVer };
|
package/package.json
CHANGED
|
@@ -1,70 +1,59 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"612.js",
|
|
60
|
-
"711.js",
|
|
61
|
-
"LICENSE",
|
|
62
|
-
"README.md",
|
|
63
|
-
"bin/runtime-resolver.js",
|
|
64
|
-
"index.d.ts",
|
|
65
|
-
"index.js",
|
|
66
|
-
"json-schema.js",
|
|
67
|
-
"package.json",
|
|
68
|
-
"tsdoc-metadata.json"
|
|
69
|
-
]
|
|
2
|
+
"name": "runtime-resolver",
|
|
3
|
+
"version": "0.3.14",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Resolve semver-compatible versions of Node.js, Bun, and Deno runtimes with Effect-based architecture and offline fallback.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"node",
|
|
8
|
+
"bun",
|
|
9
|
+
"deno",
|
|
10
|
+
"runtime",
|
|
11
|
+
"version",
|
|
12
|
+
"semver",
|
|
13
|
+
"resolver",
|
|
14
|
+
"ci",
|
|
15
|
+
"github-actions",
|
|
16
|
+
"effect",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/spencerbeggs/runtime-resolver#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/spencerbeggs/runtime-resolver/issues"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/spencerbeggs/runtime-resolver.git"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": {
|
|
29
|
+
"name": "C. Spencer Beggs",
|
|
30
|
+
"email": "spencer@beggs.codes",
|
|
31
|
+
"url": "https://spencerbeg.gs"
|
|
32
|
+
},
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"type": "module",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./index.d.ts",
|
|
38
|
+
"import": "./index.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"bin": {
|
|
43
|
+
"runtime-resolver": "bin/runtime-resolver.js"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@octokit/auth-app": "^8.2.0",
|
|
47
|
+
"octokit": "^5.0.5",
|
|
48
|
+
"semver-effect": "^0.2.1"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@effect/cli": ">=0.75.2",
|
|
52
|
+
"@effect/platform": ">=0.96.0",
|
|
53
|
+
"@effect/platform-node": ">=0.107.0",
|
|
54
|
+
"effect": ">=3.21.0"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=24.11.0"
|
|
58
|
+
}
|
|
70
59
|
}
|