v3.2.0
release: v3.2.0 (#77)
feat(versioning): add API path versioning and shutdown race fix (v3.2.0)
Squashes 42 commits from release/3.2.0 (e830379..a72358e) into master.
What's new
API Path Versioning (VersionManager)
Register the same logical API path multiple times under different version
tags. A configurable discriminator routes each caller to the correct
version at property-access time. Both the versioned namespaces and the
logical dispatcher path are live simultaneously.
const api = await slothlet({ dir: "./api", versionDispatcher: "version" });
await api.slothlet.api.add("auth", "./api/v1", {}, { version: "v1", default: true, metadata: { stable: true } });
await api.slothlet.api.add("auth", "./api/v2", {}, { version: "v2", metadata: { stable: true } });
// Direct versioned access — always bypasses dispatcher
api.v1.auth.login(user, pass);
api.v2.auth.login(user, pass, mfa);
// Logical dispatcher — routes to whichever version matches caller metadata
api.auth.login(user, pass, mfa);The versionDispatcher config key accepts a string (metadata key lookup)
or a function (allVersions, caller) => versionTag | null. Omitting it
behaves identically to "version".
Default version resolution: explicit default: true wins; otherwise the
highest semver-parsed tag wins (leading v/V prefix and pre-release
suffixes stripped for comparison, original tag preserved).
Inline force override via context[Symbol.for("slothlet.versioning.force")] = "v1".
api.slothlet.versioning runtime API
list(logicalPath)— returns{ versions, default }snapshot orundefinedsetDefault(logicalPath, versionTag)— override default at runtimeunregister(logicalPath, versionTag)— remove a version; tears down
dispatcher when last version is removedgetVersionMetadata(logicalPath, versionTag)— retrieve VersionManager-only metadatasetVersionMetadata(logicalPath, versionTag, patch)— patch VersionManager-only metadata at runtime
Separate version metadata store
options.metadata (regular Metadata system) and versionConfig.metadata
(VersionManager) are stored and surfaced separately and are never merged.
The function discriminator receives both in distinct metadata /
versionMetadata fields on allVersions entries and caller.
New SlothletOptions typedef properties
versionDispatcher?: string | Function | null— new config option introduced in this release; typedef documents the new keyslothlet.versioning.*block onSlothletAPItypedef covering the new versioning namespace
Coverage badge in CI
Automated coverage badge generation added to the CI pipeline. The badge
is pushed to the repo on every merge to master and on explicit
workflow_dispatch runs. Fork PRs are excluded (no GPG secrets available).
The README now displays the live coverage badge.
Shutdown race fix (lazy mode)
_drainInFlightLoads() is called at the top of shutdown() before any
teardown runs. It walks the API tree depth-first (cycle-safe, ≤15 levels,
skips version dispatcher proxies) and collects every pending
materializationPromise. Promise.allSettled() waits for all in-flight
ESM import() calls to settle before proceeding.
Root cause: in lazy mode, first-access and api.slothlet.api.add() calls
trigger background dynamic import(). When shutdown() fired immediately
after (e.g. in a test afterEach), the worker process could exit before
those promises settled, producing unhandled MODULE_IMPORT_FAILED
rejections and exit code 1 even though all tests passed.
Fixes included in this squash
- Two api-manager test files used hardcoded
/srv/repos/slothlet/package.json
paths that only exist on the local dev machine. In CIfs.stat()throws
ENOENTinstead of returning a file stat, soresolveFolderPathlines
276 (!isDirectory()) and 283 (SlothletError re-throw) were never hit.
Both replaced withpath.resolve(____dirname, "../../../../package.json"). - The JSDoc for
api.slothlet.api.add()declared\@param folderPathasstring
and\@returns Promise<string>, but the function passesfolderPathdirectly
toaddApiComponent()which handles arrays and returnsstring[]. Updated to
string|string[]andPromise<string|string[]>. removeApiComponent()inapi-manager.mjsusedArray.prototype.findLast()
to prefer the most recently registered moduleID when multiple entries matched.
findLastwas introduced in Node 18.0.0; the package declares
engines.node >=16.20.2, so this threwTypeErroron the minimum supported
runtime. Replaced with an equivalent reverse scan.
New files
src/lib/handlers/version-manager.mjs— VersionManager componentdocs/VERSIONING.md— full user-facing versioning referencedocs/changelog/v3/v3.2.0.md— release changelogapi_tests/api_test_versioned/— versioning test fixturestests/vitests/suites/versioning/— 14 versioning test suites
Squashed commits (oldest → newest)
- e830379 feat(i18n): add versioning i18n keys to all 11 language files
- 112075d feat(versioning): add VersionManager class and versionDispatcher config
- 84b1d4f feat(versioning): integrate VersionManager into framework lifecycle
- 4d05ef7 test(versioning): add fixtures and test suites for API path versioning
- c3512b5 docs(versioning): add VERSIONING.md user-facing feature reference
- 0520e31 chore: bump version to 3.2.0
- 1daf856 fix(shutdown): drain in-flight lazy-mode module loads before teardown
- aab2128 test(versioning): improve version-manager coverage (~100%)
- 621f634 fix(versioning): annotate all structurally-unreachable branches in version-manager
- 3871411 test(coverage): replace v8 ignores with real tests for i18n/versioning gaps
- a465cf4 docs(versioning): add v3.2.0 changelog and update README and VERSIONING.md
- f3cc3d6 ci: update CI configuration for coverage badge and Node.js version
- 803d0a3 docs(readme): add coverage badge to README and update links
- 5bacbc5 fix(versioning): sync #dispatchers when logical path is removed externally
- e6eeba4 ci: update type check command to use ci:test:types script
- db59a61 fix(versioning): correct versionMeta spread order and list() return type
- 01b36b0 fix(i18n): update error messages in multiple language files for clarity
- d984fc9 chore: trigger CI
- bd444c2 fix(versioning): correct list() return type and guard in api_builder
- f787fed docs(versioning): remove VERSION_DISCRIMINATOR_INVALID_RETURN from error table
- 876352c feat(versioning): add slothlet.versioning namespace to SlothletAPI typedef and versionDispatcher to SlothletOptions
- 5a5850a fix(tests): replace hardcoded /srv/repos/slothlet paths with ____dirname-relative paths
- fd2b950 fix(versioning): use effectivePath for ownership registration and materialization drain
- 537cc3e fix(shutdown): use resolveWrapper() instead of Object.hasOwn() in _drainInFlightLoads
- de4fc68 fix(types): add null to versionDispatcher type in SlothletOptions
- 371e5ba docs(readme): lowercase coverage badge image reference to match definition
- 16d16fe fix(versioning): avoid split('.') on effectivePath; update folderPath JSDoc
- f733e3d fix(ci): gate coverage badge push to master branch or workflow_dispatch
- 5dfb4d6 fix(versioning,shutdown): fix three bugs from Copilot review
- 1c89068 docs(versioning): add null to versionDispatcher type in VERSIONING.md
- 78fe5c0 fix(versioning): key metadata registration off effectiveParts[0] not normalizedPath
- 4c1e749 fix(versioning): rollback mounted subtree when registerVersion throws
- 63b8b15 refactor(versioning): remove redundant rollback helper methods
- 1ca4564 fix(versioning): scrub addHistory before removeApiComponent in rollback
- 85a3a0a fix(docs,i18n): correct versioning namespace availability claim and Hindi punctuation
- 59afabd docs(versioning): clarify slothlet.versioning sub-property availability in JSDoc
- e41e19b fix(versioning): fix lookup/removal/dispatch for dotted version tags (e.g. "2.3.0")
- d0500ef fix(versioning): replace findLastIndex with reverse loop in rollback
- b119be4 fix(core): replace remaining findLast usage for node 16 compatibility
- 20f3250 fix(versioning): change getVersionMetadata to (logicalPath, versionTag); add setVersionMetadata
- dc69abb feat(versioning): add metadata.setForVersion/getForVersion convenience methods
- a72358e docs(versioning): update VERSIONING.md and changelog for new metadata APIs