@cldmv/slothlet 3.1.0 → 3.2.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 (32) hide show
  1. package/AGENT-USAGE.md +129 -0
  2. package/README.md +11 -9
  3. package/dist/lib/builders/api_builder.mjs +99 -4
  4. package/dist/lib/handlers/api-manager.mjs +179 -14
  5. package/dist/lib/handlers/metadata.mjs +17 -0
  6. package/dist/lib/handlers/unified-wrapper.mjs +38 -11
  7. package/dist/lib/handlers/version-manager.mjs +828 -0
  8. package/dist/lib/helpers/config.mjs +20 -5
  9. package/dist/lib/i18n/languages/de-de.json +15 -1
  10. package/dist/lib/i18n/languages/en-gb.json +15 -1
  11. package/dist/lib/i18n/languages/en-us.json +15 -1
  12. package/dist/lib/i18n/languages/es-mx.json +15 -1
  13. package/dist/lib/i18n/languages/fr-fr.json +15 -1
  14. package/dist/lib/i18n/languages/hi-in.json +15 -1
  15. package/dist/lib/i18n/languages/ja-jp.json +15 -1
  16. package/dist/lib/i18n/languages/ko-kr.json +15 -1
  17. package/dist/lib/i18n/languages/pt-br.json +15 -1
  18. package/dist/lib/i18n/languages/ru-ru.json +15 -1
  19. package/dist/lib/i18n/languages/zh-cn.json +15 -1
  20. package/dist/slothlet.mjs +70 -1
  21. package/package.json +5 -2
  22. package/types/dist/lib/builders/api_builder.d.mts.map +1 -1
  23. package/types/dist/lib/handlers/api-manager.d.mts +28 -0
  24. package/types/dist/lib/handlers/api-manager.d.mts.map +1 -1
  25. package/types/dist/lib/handlers/metadata.d.mts +15 -0
  26. package/types/dist/lib/handlers/metadata.d.mts.map +1 -1
  27. package/types/dist/lib/handlers/unified-wrapper.d.mts.map +1 -1
  28. package/types/dist/lib/handlers/version-manager.d.mts +234 -0
  29. package/types/dist/lib/handlers/version-manager.d.mts.map +1 -0
  30. package/types/dist/lib/helpers/config.d.mts.map +1 -1
  31. package/types/dist/slothlet.d.mts +15 -0
  32. package/types/dist/slothlet.d.mts.map +1 -1
package/AGENT-USAGE.md CHANGED
@@ -409,6 +409,34 @@ export const secureOperation = {
409
409
 
410
410
  ---
411
411
 
412
+ ## 🌍 Environment Snapshot (v3.1+)
413
+
414
+ Slothlet captures a **frozen snapshot of `process.env` at init time** and exposes it at `api.slothlet.env`. The snapshot is deeply read-only — mutating `process.env` after init does not affect `api.slothlet.env`.
415
+
416
+ ```js
417
+ const api = await slothlet({
418
+ dir: "./api",
419
+ env: true,
420
+ // env: { include: ["NODE_ENV", "DATABASE_URL", "PORT"] } // allowlist
421
+ });
422
+
423
+ // Access inside any module:
424
+ import { self } from "@cldmv/slothlet/runtime";
425
+
426
+ export const getConfig = () => ({
427
+ mode: self.slothlet.env.NODE_ENV,
428
+ dbUrl: self.slothlet.env.DATABASE_URL
429
+ });
430
+ ```
431
+
432
+ **Key behaviors:**
433
+ - `env: true` → all `process.env` variables are captured
434
+ - `env: { include: [...] }` → only the listed keys are captured (recommended for security)
435
+ - `api.slothlet.env` is a frozen object — writes throw in strict mode
436
+ - Snapshot is taken at init time — late `process.env` mutations are NOT reflected
437
+
438
+ ---
439
+
412
440
  ## 🔁 Hot Reload / Dynamic API Management
413
441
 
414
442
  ```js
@@ -457,6 +485,69 @@ api.slothlet.lifecycle.off("materialized:complete", handler);
457
485
 
458
486
  ---
459
487
 
488
+ ## 🔀 API Path Versioning (v3.2+)
489
+
490
+ Mount multiple versions of the same logical path and dispatch to the correct version automatically based on the caller's version metadata.
491
+
492
+ ### Setup
493
+
494
+ ```js
495
+ const api = await slothlet({
496
+ dir: "./api",
497
+ versionDispatcher: "version" // use caller's versionMetadata.version field
498
+ // versionDispatcher: (allVersions, caller) => { ... } // custom function
499
+ });
500
+
501
+ // Register versioned modules — 4th argument is versionConfig
502
+ await api.slothlet.api.add("auth", "./api/v1", {}, { version: "v1", default: true });
503
+ await api.slothlet.api.add("auth", "./api/v2", {}, { version: "v2" });
504
+ ```
505
+
506
+ ### Access Patterns
507
+
508
+ ```js
509
+ // Dispatcher — routes via discriminator (use this in most cases)
510
+ api.auth.login(user, pass) // → routes to v1 or v2 based on caller metadata
511
+
512
+ // Direct versioned access — bypasses dispatcher entirely
513
+ api.v1.auth.login(user, pass) // → always v1
514
+ api.v2.auth.login(user, pass) // → always v2
515
+ ```
516
+
517
+ ### Attaching Version Metadata to a Caller
518
+
519
+ ```js
520
+ // Register a module WITH a version tag so the discriminator can use it
521
+ await api.slothlet.api.add("services/payments", "./payments", {}, {
522
+ version: "v2",
523
+ metadata: { stable: true } // versionConfig.metadata — stored in VersionManager only
524
+ });
525
+
526
+ // options.metadata (3rd arg) → regular Metadata system (metadata.caller() etc.)
527
+ // versionConfig.metadata (4th arg) → version-system only, used by the discriminator
528
+ ```
529
+
530
+ ### Runtime Version Management
531
+
532
+ ```js
533
+ // List registered versions for a path
534
+ api.slothlet.versioning.list("auth");
535
+ // → { versions: { v1: { ... }, v2: { ... } }, default: "v2" }
536
+
537
+ // Change the default dispatcher fallback
538
+ api.slothlet.versioning.setDefault("auth", "v1");
539
+
540
+ // Unregister a version (removes api.v1.auth; dispatcher updates automatically)
541
+ await api.slothlet.versioning.unregister("auth", "v1");
542
+
543
+ // Read version metadata stored at registration
544
+ api.slothlet.versioning.getVersionMetadata(moduleID);
545
+ ```
546
+
547
+ > 📖 See [`docs/VERSIONING.md`](docs/VERSIONING.md) for full documentation.
548
+
549
+ ---
550
+
460
551
  ## 📁 File Organization Best Practices
461
552
 
462
553
  ### ✅ Clean Folder Structure
@@ -550,6 +641,41 @@ api.slothlet.lifecycle.on("materialized:complete", handler);
550
641
  api.slothlet.lifecycle.off("materialized:complete", handler);
551
642
  ```
552
643
 
644
+ ### ❌ Mistake 6: Assuming `api.v1.auth` Goes Through the Dispatcher
645
+
646
+ ```js
647
+ // ❌ WRONG — direct versioned path bypasses the discriminator entirely
648
+ api.v1.auth.login(user, pass); // always v1, no routing logic
649
+
650
+ // ✅ CORRECT — use the logical dispatcher path for dynamic routing
651
+ api.auth.login(user, pass); // routes based on caller version metadata
652
+ ```
653
+
654
+ ### ❌ Mistake 7: Wrong Config Key for Versioning
655
+
656
+ ```js
657
+ // ❌ WRONG
658
+ const api = await slothlet({ dir: "./api", versionResolver: "version" });
659
+
660
+ // ✅ CORRECT
661
+ const api = await slothlet({ dir: "./api", versionDispatcher: "version" });
662
+ ```
663
+
664
+ ### ❌ Mistake 8: Conflating versionConfig.metadata with options.metadata
665
+
666
+ ```js
667
+ // ❌ WRONG — puts version tag in the regular Metadata system instead of VersionManager
668
+ await api.slothlet.api.add("auth", "./v2", { metadata: { version: "v2" } });
669
+
670
+ // ✅ CORRECT
671
+ // options.metadata (3rd arg) → regular Metadata system (metadata.caller() etc.)
672
+ // versionConfig.metadata (4th arg) → VersionManager only, used by the discriminator
673
+ await api.slothlet.api.add("auth", "./v2",
674
+ { metadata: { role: "core" } },
675
+ { version: "v2", metadata: { stable: true } }
676
+ );
677
+ ```
678
+
553
679
  ---
554
680
 
555
681
  ## ✅ AI Agent Checklist
@@ -563,6 +689,9 @@ api.slothlet.lifecycle.off("materialized:complete", handler);
563
689
  - [ ] **Lifecycle** uses `api.slothlet.lifecycle.on/off()` only
564
690
  - [ ] **Lazy mode**: if using background materialization, use `api.slothlet.materialize.wait()` before accessing the API
565
691
  - [ ] **Hook subsets**: auth/security → `subset: "before"`, main logic → `"primary"`, audit → `"after"`
692
+ - [ ] **Versioning config key is `versionDispatcher:`**, not `versionResolver:` or `versionDiscriminator:`
693
+ - [ ] **Use `api.auth` (dispatcher path) for dynamic routing**, not `api.v1.auth` (direct path bypasses discriminator)
694
+ - [ ] **`versionConfig.metadata` (4th arg)** and **`options.metadata` (3rd arg)** are separate systems — don't conflate them
566
695
  - [ ] **Double quotes everywhere** - follow Slothlet coding standards
567
696
 
568
697
  ---
package/README.md CHANGED
@@ -16,7 +16,7 @@ The name might suggest we're taking it easy, but don't be fooled. **Slothlet del
16
16
 
17
17
  > _Where sophisticated architecture meets blazing performance - slothlet is anything but slow._
18
18
 
19
- [![npm version]][npm_version_url] [![npm downloads]][npm_downloads_url] <!-- [![GitHub release]][github_release_url] -->[![GitHub downloads]][github_downloads_url] [![Last commit]][last_commit_url] <!-- [![Release date]][release_date_url] -->[![npm last update]][npm_last_update_url]
19
+ [![npm version]][npm_version_url] [![npm downloads]][npm_downloads_url] <!-- [![GitHub release]][github_release_url] -->[![GitHub downloads]][github_downloads_url] [![Last commit]][last_commit_url] <!-- [![Release date]][release_date_url] -->[![npm last update]][npm_last_update_url] [![coverage]][coverage_url]
20
20
 
21
21
  > [!NOTE]
22
22
  > **🚀 Production Ready Modes:**
@@ -55,20 +55,19 @@ Every feature has been hardened with a comprehensive test suite - over **5,300 t
55
55
 
56
56
  ## ✨ What's New
57
57
 
58
- ### Latest: v3.1.0 (March 2026)
58
+ ### Latest: v3.2.1 (April 2026)
59
59
 
60
- - **Environment Snapshot** — `api.slothlet.env` exposes a frozen copy of `process.env` captured at initialization time; every module accesses it via `self.slothlet.env`
61
- - **`env.include` Allowlist** — restrict the snapshot to specific keys with `env: { include: ["NODE_ENV", "PORT"] }`; empty array falls back to full snapshot
62
- - **Reload Immunity** — snapshot is captured once on first `load()` and never replaced on subsequent `api.slothlet.reload()` calls (including partial `api.slothlet.api.reload()` calls)
63
- - [View full v3.1.0 Changelog](./docs/changelog/v3/v3.1.0.md)
60
+ - **Missing `defineProperty` trap** — the version dispatcher proxy introduced in v3.2.0 had no `defineProperty` trap; calls fell through to the raw target, bypassing version routing, and non-configurable writes could trigger V8 proxy invariant `TypeError` on subsequent reads
61
+ - **Pre-commit tooling** — removed duplicate `build:cleanup` step from `precommit-validation.mjs`; sequence is now `build:dev debug test:node vitest`
62
+ - [View full v3.2.1 Changelog](./docs/changelog/v3/v3.2.1.md)
64
63
 
65
64
  ### Recent Releases
66
65
 
66
+ - **v3.2.0** (April 2026) — API Path Versioning (`versionDispatcher`, `api.slothlet.versioning.*`, version metadata, dispatcher proxy); lazy-mode shutdown race fix ([Changelog](./docs/changelog/v3/v3.2.0.md))
67
+ - **v3.1.0** (March 2026) — Frozen `api.slothlet.env` snapshot; `env.include` allowlist; reload immunity ([Changelog](./docs/changelog/v3/v3.1.0.md))
67
68
  - **v3.0.1** (March 2026) — Resolver fix for user `index.mjs` mis-classified as internal; CI `slothlet-dev` stripping hardening; respawn race fix; resilient `build:dist` script ([Changelog](./docs/changelog/v3/v3.0.1.md))
68
69
  - **v3.0.0** (February 2026) — Unified Wrapper architecture, redesigned hook system, full i18n, background materialization, lifecycle events, collision modes, mutation controls ([Changelog](./docs/changelog/v3.0.md))
69
- - **v2.11.0** — AddApi Special File Pattern (Rule 11), smart flattening enhancements ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.11.md))
70
- - **v2.10.0** — Function metadata tagging and introspection capabilities ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.10.md))
71
- - **v2.9** — Per-Request Context Isolation ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.9.md))
70
+
72
71
 
73
72
  📚 **For complete version history and detailed release notes, see [docs/changelog/](./docs/changelog/) folder.**
74
73
 
@@ -327,6 +326,7 @@ await api.slothlet.api.reload("database.*");
327
326
  | `backgroundMaterialize` | `boolean` | `false` | In lazy mode: start background pre-loading of all modules immediately after init; automatically enables materialization tracking and the `materialized:complete` lifecycle event |
328
327
  | `api.collision` | `mixed` | `"merge"` | Collision mode for API namespace conflicts: `"merge"`, `"skip"`, `"overwrite"`, `"throw"` - or `{ initial: "merge", api: "skip" }` to set independently for load vs runtime `add()` |
329
328
  | `api.mutations` | `object` | all `true` | Per-operation mutation controls: `{ add: true, remove: true, reload: true }` - set any to `false` to disable |
329
+ | `versionDispatcher` | `mixed` | `undefined` | Version routing discriminator: `"version"` (or any string key) looks up that key in the caller's version metadata; a function receives `(allVersions, caller)` and returns a tag or `null`; `undefined` behaves like `"version"` |
330
330
  | `i18n` | `object` | `{}` | Internationalization settings: `{ language: "en" }` - supported: `en`, `es`, `fr`, `de`, `pt`, `it`, `ja`, `zh`, `ko` |
331
331
 
332
332
  ---
@@ -1035,6 +1035,8 @@ To my wife and children - thank you for your patience, your encouragement, and t
1035
1035
  [github_license_url]: https://github.com/CLDMV/slothlet/blob/HEAD/LICENSE
1036
1036
  [npm license]: https://img.shields.io/npm/l/%40cldmv%2Fslothlet.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
1037
1037
  [npm_license_url]: https://www.npmjs.com/package/@cldmv/slothlet
1038
+ [coverage]: https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FCLDMV%2Fslothlet%2Fbadges%2Fcoverage.json&style=for-the-badge&logo=vitest&logoColor=white
1039
+ [coverage_url]: https://github.com/CLDMV/slothlet/blob/badges/coverage.json
1038
1040
  [contributors]: https://img.shields.io/github/contributors/CLDMV/slothlet.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
1039
1041
  [contributors_url]: https://github.com/CLDMV/slothlet/graphs/contributors
1040
1042
  [sponsor shinrai]: https://img.shields.io/github/sponsors/shinrai?style=for-the-badge&logo=githubsponsors&logoColor=white&labelColor=EA4AAA&label=Sponsor
@@ -30,7 +30,14 @@ function _resolvePathOrModuleId(slothlet, pathOrModuleId) {
30
30
 
31
31
 
32
32
  if (history) {
33
- const match = history.findLast((entry) => entry.moduleID === pathOrModuleId);
33
+ let match = null;
34
+ for (let i = history.length - 1; i >= 0; i--) {
35
+ const entry = history[i];
36
+ if (entry?.moduleID === pathOrModuleId) {
37
+ match = entry;
38
+ break;
39
+ }
40
+ }
34
41
  if (match) return match.apiPath;
35
42
  }
36
43
  return pathOrModuleId;
@@ -186,7 +193,7 @@ export class ApiBuilder extends ComponentBase {
186
193
 
187
194
  api: {
188
195
 
189
- add: async function slothlet_api_add(apiPath, folderPath, options = {}) {
196
+ add: async function slothlet_api_add(apiPath, folderPath, options = {}, versionConfig = null) {
190
197
 
191
198
  if (!config.api?.mutations?.add) {
192
199
  throw new slothlet.SlothletError("INVALID_CONFIG_MUTATIONS_DISABLED", {
@@ -208,7 +215,8 @@ export class ApiBuilder extends ComponentBase {
208
215
  return slothlet.handlers.apiManager.addApiComponent({
209
216
  apiPath,
210
217
  folderPath,
211
- options: filteredOptions
218
+ options: filteredOptions,
219
+ versionConfig: versionConfig || null
212
220
  });
213
221
  },
214
222
 
@@ -568,6 +576,39 @@ export class ApiBuilder extends ComponentBase {
568
576
  if (!slothlet.handlers?.metadata) return;
569
577
  const resolvedPath = _resolvePathOrModuleId(slothlet, pathOrModuleId);
570
578
  return slothlet.handlers.metadata.removePathMetadata(resolvedPath, key);
579
+ },
580
+
581
+
582
+ setForVersion: function slothlet_metadata_setForVersion(logicalPath, versionTag, keyOrObj, value) {
583
+ if (!slothlet.handlers?.metadata) {
584
+ throw new slothlet.SlothletError("METADATA_NOT_AVAILABLE", {
585
+ handlersKeys: slothlet.handlers
586
+ ? Object.keys(slothlet.handlers).join(", ")
587
+ : "undefined",
588
+ validationError: true
589
+ });
590
+ }
591
+ const info = slothlet.handlers?.versionManager?.list(logicalPath);
592
+ if (!info || !info.versions?.[versionTag]) {
593
+ throw new slothlet.SlothletError("VERSION_NOT_FOUND", {
594
+ version: versionTag,
595
+ apiPath: logicalPath
596
+ });
597
+ }
598
+ const { moduleID } = info.versions[versionTag];
599
+ const resolvedPath = _resolvePathOrModuleId(slothlet, moduleID);
600
+ return slothlet.handlers.metadata.setPathMetadata(resolvedPath, keyOrObj, value);
601
+ },
602
+
603
+
604
+ getForVersion: function slothlet_metadata_getForVersion(logicalPath, versionTag) {
605
+
606
+ if (!slothlet.handlers?.metadata) return {};
607
+ const info = slothlet.handlers?.versionManager?.list(logicalPath);
608
+ if (!info || !info.versions?.[versionTag]) return {};
609
+ const { moduleID } = info.versions[versionTag];
610
+ const resolvedPath = _resolvePathOrModuleId(slothlet, moduleID);
611
+ return slothlet.handlers.metadata.getPathMetadata(resolvedPath);
571
612
  }
572
613
  },
573
614
 
@@ -639,7 +680,61 @@ export class ApiBuilder extends ComponentBase {
639
680
  })(),
640
681
 
641
682
 
642
- env: slothlet.envSnapshot
683
+ env: slothlet.envSnapshot,
684
+
685
+
686
+ versioning: {
687
+
688
+ list: function slothlet_version_list(logicalPath) {
689
+
690
+
691
+ if (!slothlet.handlers?.versionManager) return undefined;
692
+ return slothlet.handlers.versionManager.list(logicalPath);
693
+ },
694
+
695
+
696
+ setDefault: function slothlet_version_setDefault(logicalPath, versionTag) {
697
+
698
+
699
+ if (!slothlet.handlers?.versionManager) return;
700
+ return slothlet.handlers.versionManager.setDefault(logicalPath, versionTag);
701
+ },
702
+
703
+
704
+ unregister: async function slothlet_version_unregister(logicalPath, versionTag) {
705
+
706
+
707
+ if (!slothlet.handlers?.versionManager) return false;
708
+
709
+ const info = slothlet.handlers.versionManager.list(logicalPath);
710
+ if (!info || !info.versions?.[versionTag]) return false;
711
+
712
+
713
+
714
+ const { moduleID: versionedModuleID } = info.versions[versionTag];
715
+
716
+
717
+
718
+ await slothlet.handlers.apiManager.removeApiComponent(versionedModuleID);
719
+ return true;
720
+ },
721
+
722
+
723
+ getVersionMetadata: function slothlet_version_getVersionMetadata(logicalPath, versionTag) {
724
+
725
+
726
+ if (!slothlet.handlers?.versionManager) return undefined;
727
+ return slothlet.handlers.versionManager.getVersionMetadataByPath(logicalPath, versionTag);
728
+ },
729
+
730
+
731
+ setVersionMetadata: function slothlet_version_setVersionMetadata(logicalPath, versionTag, patch) {
732
+
733
+
734
+ if (!slothlet.handlers?.versionManager) return;
735
+ return slothlet.handlers.versionManager.setVersionMetadataByPath(logicalPath, versionTag, patch);
736
+ }
737
+ }
643
738
  };
644
739
 
645
740
 
@@ -863,7 +863,7 @@ export class ApiManager extends ComponentBase {
863
863
  async addApiComponent(params) {
864
864
 
865
865
 
866
- const { apiPath, folderPath, options = {} } = params || {};
866
+ const { apiPath, folderPath, options = {}, versionConfig = null } = params || {};
867
867
 
868
868
 
869
869
  if (Array.isArray(folderPath)) {
@@ -872,7 +872,8 @@ export class ApiManager extends ComponentBase {
872
872
  const moduleID = await this.addApiComponent({
873
873
  apiPath,
874
874
  folderPath: singlePath,
875
- options
875
+ options,
876
+ versionConfig
876
877
  });
877
878
  moduleIDs.push(moduleID);
878
879
  }
@@ -890,6 +891,37 @@ export class ApiManager extends ComponentBase {
890
891
  const { apiPath: normalizedPath, parts } = this.normalizeApiPath(apiPath);
891
892
 
892
893
 
894
+ let effectivePath = normalizedPath;
895
+ let effectiveParts = parts;
896
+ if (versionConfig?.version !== undefined && versionConfig?.version !== null) {
897
+
898
+
899
+
900
+ if (normalizedPath === "") {
901
+ throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID", {
902
+ apiPath,
903
+ reason: translate("API_PATH_REASON_VERSIONED_ROOT"),
904
+ index: undefined,
905
+ segment: undefined,
906
+ validationError: true
907
+ });
908
+ }
909
+ if (typeof versionConfig.version !== "string" || !String(versionConfig.version).trim()) {
910
+ throw new this.SlothletError("INVALID_CONFIG_VERSION_TAG", {
911
+ received: versionConfig.version,
912
+ validationError: true
913
+ });
914
+ }
915
+ const versionTag = String(versionConfig.version).trim();
916
+
917
+
918
+
919
+
920
+ effectiveParts = [versionTag, ...parts];
921
+ effectivePath = effectiveParts.join(".");
922
+ }
923
+
924
+
893
925
  const { resolvedPath, isDirectory, isFile } = await this.resolvePath(folderPath);
894
926
 
895
927
 
@@ -959,7 +991,8 @@ export class ApiManager extends ComponentBase {
959
991
 
960
992
 
961
993
 
962
- apiPathPrefix: normalizedPath,
994
+
995
+ apiPathPrefix: effectivePath,
963
996
  collisionContext: "addApi",
964
997
  moduleID: moduleID,
965
998
 
@@ -973,7 +1006,7 @@ export class ApiManager extends ComponentBase {
973
1006
 
974
1007
  if (this.slothlet.handlers.apiCacheManager) {
975
1008
  this.slothlet.handlers.apiCacheManager.set(moduleID, {
976
- endpoint: normalizedPath,
1009
+ endpoint: effectivePath,
977
1010
  moduleID: moduleID,
978
1011
  api: newApi,
979
1012
  folderPath: resolvedFolderPath,
@@ -1141,7 +1174,7 @@ export class ApiManager extends ComponentBase {
1141
1174
  const isCallableNamespace = typeof apiToMerge === "function";
1142
1175
 
1143
1176
  const containerWrapper = new UnifiedWrapper(this.slothlet, {
1144
- apiPath: normalizedPath,
1177
+ apiPath: effectivePath,
1145
1178
  mode: this.____config.mode,
1146
1179
  isCallable: isCallableNamespace,
1147
1180
  moduleID: moduleID,
@@ -1154,14 +1187,14 @@ export class ApiManager extends ComponentBase {
1154
1187
  apiToMerge = containerWrapper.createProxy();
1155
1188
  }
1156
1189
 
1157
- const result1 = await this.setValueAtPath(this.slothlet.api, parts, apiToMerge, {
1190
+ const result1 = await this.setValueAtPath(this.slothlet.api, effectiveParts, apiToMerge, {
1158
1191
  mutateExisting,
1159
1192
  collisionMode,
1160
1193
  moduleID,
1161
1194
  sourceFolder: resolvedFolderPath
1162
1195
  });
1163
1196
 
1164
- const result2 = await this.setValueAtPath(this.slothlet.boundApi, parts, apiToMerge, {
1197
+ const result2 = await this.setValueAtPath(this.slothlet.boundApi, effectiveParts, apiToMerge, {
1165
1198
  mutateExisting,
1166
1199
  collisionMode,
1167
1200
  moduleID,
@@ -1187,6 +1220,11 @@ export class ApiManager extends ComponentBase {
1187
1220
 
1188
1221
  const collectPendingMaterializations = (obj, depth = 0) => {
1189
1222
  if (!obj || typeof obj !== "object" || depth > 10) return;
1223
+
1224
+
1225
+
1226
+
1227
+ if (obj.__isVersionDispatcher === true) return;
1190
1228
 
1191
1229
  const wrapper = resolveWrapper(obj);
1192
1230
  if (wrapper) {
@@ -1220,7 +1258,9 @@ export class ApiManager extends ComponentBase {
1220
1258
 
1221
1259
 
1222
1260
 
1223
- if (parts.length === 0) {
1261
+
1262
+
1263
+ if (effectiveParts.length === 0) {
1224
1264
 
1225
1265
  for (const key of Object.keys(newApi)) {
1226
1266
 
@@ -1232,7 +1272,7 @@ export class ApiManager extends ComponentBase {
1232
1272
  } else {
1233
1273
 
1234
1274
  let current = this.slothlet.api;
1235
- for (const part of parts) {
1275
+ for (const part of effectiveParts) {
1236
1276
 
1237
1277
 
1238
1278
  if (current && current[part]) {
@@ -1277,7 +1317,11 @@ export class ApiManager extends ComponentBase {
1277
1317
  this.slothlet.handlers.metadata.registerUserMetadata(key, metadata);
1278
1318
  }
1279
1319
  } else {
1280
- const rootSegment = normalizedPath.split(".")[0];
1320
+
1321
+
1322
+
1323
+
1324
+ const rootSegment = effectiveParts[0];
1281
1325
  this.slothlet.handlers.metadata.registerUserMetadata(rootSegment, metadata);
1282
1326
  }
1283
1327
  }
@@ -1285,8 +1329,10 @@ export class ApiManager extends ComponentBase {
1285
1329
 
1286
1330
 
1287
1331
 
1332
+
1333
+
1288
1334
  if (this.slothlet.handlers.ownership && moduleID) {
1289
- this.slothlet.handlers.ownership.registerSubtree(apiToMerge, moduleID, normalizedPath);
1335
+ this.slothlet.handlers.ownership.registerSubtree(apiToMerge, moduleID, effectivePath);
1290
1336
  }
1291
1337
 
1292
1338
 
@@ -1297,24 +1343,83 @@ export class ApiManager extends ComponentBase {
1297
1343
  apiPath: normalizedPath,
1298
1344
  folderPath: resolvedFolderPath,
1299
1345
  options: { ...restOptions, metadata, moduleID },
1300
- moduleID
1346
+ moduleID,
1347
+ versionConfig: versionConfig || null
1301
1348
  });
1302
1349
 
1303
1350
 
1351
+
1304
1352
  this.state.operationHistory.push({
1305
1353
  type: "add",
1306
1354
  apiPath: normalizedPath,
1307
1355
  folderPath: resolvedFolderPath,
1308
1356
  options: { ...restOptions, metadata, moduleID },
1309
- moduleID
1357
+ moduleID,
1358
+ versionConfig: versionConfig || null
1310
1359
  });
1311
1360
  }
1312
1361
  }
1313
1362
 
1363
+
1364
+
1365
+
1366
+
1367
+ if (versionConfig?.version && this.slothlet.handlers.versionManager) {
1368
+ const versionTag = String(versionConfig.version).trim();
1369
+ try {
1370
+ this.slothlet.handlers.versionManager.registerVersion(
1371
+ normalizedPath,
1372
+ versionTag,
1373
+ moduleID,
1374
+ versionConfig.metadata ?? {},
1375
+ versionConfig.default ?? false
1376
+ );
1377
+ } catch (error) {
1378
+ await this._rollbackFailedVersionedAdd({ moduleID, effectivePath, normalizedPath });
1379
+ throw error;
1380
+ }
1381
+ }
1382
+
1314
1383
  return moduleID;
1315
1384
  }
1316
1385
 
1317
1386
 
1387
+ async _rollbackFailedVersionedAdd({ moduleID, effectivePath, normalizedPath }) {
1388
+
1389
+
1390
+
1391
+
1392
+
1393
+ let addIndex = -1;
1394
+ for (let i = this.state.operationHistory.length - 1; i >= 0; i--) {
1395
+ const entry = this.state.operationHistory[i];
1396
+ if (entry?.type === "add" && entry?.apiPath === normalizedPath && entry?.moduleID === moduleID) {
1397
+ addIndex = i;
1398
+ break;
1399
+ }
1400
+ }
1401
+ if (addIndex !== -1) {
1402
+ this.state.operationHistory.splice(addIndex, 1);
1403
+ }
1404
+
1405
+
1406
+
1407
+
1408
+
1409
+ this.state.addHistory = this.state.addHistory.filter((entry) => entry?.moduleID !== moduleID);
1410
+
1411
+
1412
+
1413
+
1414
+ try {
1415
+ await this.removeApiComponent(moduleID || effectivePath, { recordHistory: false });
1416
+ } catch {
1417
+
1418
+
1419
+ }
1420
+ }
1421
+
1422
+
1318
1423
  async removeApiComponent(pathOrModuleId, options = {}) {
1319
1424
  const recordHistory = options.recordHistory !== false;
1320
1425
  if (typeof pathOrModuleId !== "string" || !pathOrModuleId) {
@@ -1340,7 +1445,14 @@ export class ApiManager extends ComponentBase {
1340
1445
 
1341
1446
 
1342
1447
  const registeredModules = Array.from(this.slothlet.handlers.ownership.moduleToPath.keys());
1343
- const matchingModule = registeredModules.findLast((m) => m === candidateModuleID || m.startsWith(`${candidateModuleID}_`));
1448
+ let matchingModule = null;
1449
+ for (let i = registeredModules.length - 1; i >= 0; i--) {
1450
+ const candidate = registeredModules[i];
1451
+ if (candidate === candidateModuleID || candidate.startsWith(`${candidateModuleID}_`)) {
1452
+ matchingModule = candidate;
1453
+ break;
1454
+ }
1455
+ }
1344
1456
 
1345
1457
  if (matchingModule) {
1346
1458
 
@@ -1395,6 +1507,19 @@ export class ApiManager extends ComponentBase {
1395
1507
  this.slothlet.handlers.metadata.removeUserMetadataByApiPath(rootSegment);
1396
1508
  }
1397
1509
 
1510
+ if (this.slothlet.handlers.versionManager) {
1511
+ const versionKey = this.slothlet.handlers.versionManager.getVersionKeyForModule(moduleIDKey);
1512
+ if (versionKey) {
1513
+ this.slothlet.handlers.versionManager.unregisterVersion(versionKey.logicalPath, versionKey.versionTag);
1514
+ }
1515
+
1516
+
1517
+
1518
+ if (this.slothlet.handlers.versionManager.hasDispatcher(normalizedPath)) {
1519
+ this.slothlet.handlers.versionManager.teardownDispatcher(normalizedPath);
1520
+ }
1521
+ }
1522
+
1398
1523
  this.state.operationHistory.push({
1399
1524
  type: "remove",
1400
1525
  apiPath: normalizedPath
@@ -1451,6 +1576,16 @@ export class ApiManager extends ComponentBase {
1451
1576
  const result = this.slothlet.handlers.ownership?.unregister?.(moduleIDKey) || { removed: [], rolledBack: [] };
1452
1577
 
1453
1578
 
1579
+
1580
+
1581
+ if (this.slothlet.handlers.versionManager) {
1582
+ const versionKey = this.slothlet.handlers.versionManager.getVersionKeyForModule(moduleIDKey);
1583
+ if (versionKey) {
1584
+ this.slothlet.handlers.versionManager.unregisterVersion(versionKey.logicalPath, versionKey.versionTag);
1585
+ }
1586
+ }
1587
+
1588
+
1454
1589
  const allPaths = [...result.removed, ...result.rolledBack.map((r) => r.apiPath)];
1455
1590
 
1456
1591
 
@@ -1780,6 +1915,11 @@ export class ApiManager extends ComponentBase {
1780
1915
  key: "DEBUG_MODE_MODULE_RELOAD_COMPLETE",
1781
1916
  moduleID
1782
1917
  });
1918
+
1919
+
1920
+ if (this.slothlet.handlers.versionManager) {
1921
+ this.slothlet.handlers.versionManager.onVersionedModuleReload(moduleID);
1922
+ }
1783
1923
  }
1784
1924
 
1785
1925
 
@@ -2268,6 +2408,31 @@ export class ApiManager extends ComponentBase {
2268
2408
 
2269
2409
 
2270
2410
 
2411
+ if (parts.length > 0 && implForReload && typeof implForReload === "object") {
2412
+ const lastEndpointPart = parts[parts.length - 1];
2413
+ if (lastEndpointPart && Object.prototype.hasOwnProperty.call(implForReload, lastEndpointPart)) {
2414
+ const dupValue = implForReload[lastEndpointPart];
2415
+ const dupWrapperForDedup = resolveWrapper(dupValue);
2416
+ if (dupWrapperForDedup) {
2417
+ const hoisted = {};
2418
+ for (const k of Object.keys(implForReload)) {
2419
+ if (k !== lastEndpointPart) hoisted[k] = implForReload[k];
2420
+ }
2421
+ for (const k of Object.keys(dupWrapperForDedup).filter((k) => !k.startsWith("_") && !k.startsWith("__"))) {
2422
+ hoisted[k] = dupWrapperForDedup[k];
2423
+ }
2424
+ implForReload = hoisted;
2425
+ }
2426
+ }
2427
+ }
2428
+
2429
+
2430
+
2431
+
2432
+
2433
+
2434
+
2435
+
2271
2436
 
2272
2437
  if (implForReload && typeof implForReload === "object") {
2273
2438
  for (const key of Object.keys(implForReload)) {