@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.
- package/AGENT-USAGE.md +129 -0
- package/README.md +11 -9
- package/dist/lib/builders/api_builder.mjs +99 -4
- package/dist/lib/handlers/api-manager.mjs +179 -14
- package/dist/lib/handlers/metadata.mjs +17 -0
- package/dist/lib/handlers/unified-wrapper.mjs +38 -11
- package/dist/lib/handlers/version-manager.mjs +828 -0
- package/dist/lib/helpers/config.mjs +20 -5
- package/dist/lib/i18n/languages/de-de.json +15 -1
- package/dist/lib/i18n/languages/en-gb.json +15 -1
- package/dist/lib/i18n/languages/en-us.json +15 -1
- package/dist/lib/i18n/languages/es-mx.json +15 -1
- package/dist/lib/i18n/languages/fr-fr.json +15 -1
- package/dist/lib/i18n/languages/hi-in.json +15 -1
- package/dist/lib/i18n/languages/ja-jp.json +15 -1
- package/dist/lib/i18n/languages/ko-kr.json +15 -1
- package/dist/lib/i18n/languages/pt-br.json +15 -1
- package/dist/lib/i18n/languages/ru-ru.json +15 -1
- package/dist/lib/i18n/languages/zh-cn.json +15 -1
- package/dist/slothlet.mjs +70 -1
- package/package.json +5 -2
- package/types/dist/lib/builders/api_builder.d.mts.map +1 -1
- package/types/dist/lib/handlers/api-manager.d.mts +28 -0
- package/types/dist/lib/handlers/api-manager.d.mts.map +1 -1
- package/types/dist/lib/handlers/metadata.d.mts +15 -0
- package/types/dist/lib/handlers/metadata.d.mts.map +1 -1
- package/types/dist/lib/handlers/unified-wrapper.d.mts.map +1 -1
- package/types/dist/lib/handlers/version-manager.d.mts +234 -0
- package/types/dist/lib/handlers/version-manager.d.mts.map +1 -0
- package/types/dist/lib/helpers/config.d.mts.map +1 -1
- package/types/dist/slothlet.d.mts +15 -0
- 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
|
|
58
|
+
### Latest: v3.2.1 (April 2026)
|
|
59
59
|
|
|
60
|
-
- **
|
|
61
|
-
-
|
|
62
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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)) {
|