@cldmv/slothlet 2.8.0 → 2.10.0

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 (59) hide show
  1. package/AGENT-USAGE.md +1 -1
  2. package/README.md +300 -1557
  3. package/dist/lib/engine/slothlet_child.mjs +1 -1
  4. package/dist/lib/engine/slothlet_engine.mjs +1 -1
  5. package/dist/lib/engine/slothlet_esm.mjs +1 -1
  6. package/dist/lib/engine/slothlet_helpers.mjs +1 -1
  7. package/dist/lib/engine/slothlet_worker.mjs +1 -1
  8. package/dist/lib/helpers/als-eventemitter.mjs +5 -6
  9. package/dist/lib/helpers/api_builder/add_api.mjs +292 -0
  10. package/dist/lib/helpers/api_builder/analysis.mjs +532 -0
  11. package/dist/lib/helpers/api_builder/construction.mjs +457 -0
  12. package/dist/lib/helpers/api_builder/decisions.mjs +737 -0
  13. package/dist/lib/helpers/api_builder/metadata.mjs +248 -0
  14. package/dist/lib/helpers/api_builder.mjs +17 -1568
  15. package/dist/lib/helpers/auto-wrap.mjs +1 -1
  16. package/dist/lib/helpers/hooks.mjs +1 -1
  17. package/dist/lib/helpers/instance-manager.mjs +1 -1
  18. package/dist/lib/helpers/metadata-api.mjs +201 -0
  19. package/dist/lib/helpers/multidefault.mjs +12 -3
  20. package/dist/lib/helpers/resolve-from-caller.mjs +1 -1
  21. package/dist/lib/helpers/sanitize.mjs +1 -1
  22. package/dist/lib/helpers/utilities.mjs +121 -0
  23. package/dist/lib/modes/slothlet_eager.mjs +1 -1
  24. package/dist/lib/modes/slothlet_lazy.mjs +10 -1
  25. package/dist/lib/runtime/runtime-asynclocalstorage.mjs +49 -18
  26. package/dist/lib/runtime/runtime-livebindings.mjs +23 -4
  27. package/dist/lib/runtime/runtime.mjs +15 -4
  28. package/dist/slothlet.mjs +164 -748
  29. package/docs/API-RULES-CONDITIONS.md +508 -0
  30. package/{API-RULES.md → docs/API-RULES.md} +127 -72
  31. package/package.json +11 -9
  32. package/types/dist/lib/helpers/als-eventemitter.d.mts.map +1 -1
  33. package/types/dist/lib/helpers/api_builder/add_api.d.mts +76 -0
  34. package/types/dist/lib/helpers/api_builder/add_api.d.mts.map +1 -0
  35. package/types/dist/lib/helpers/api_builder/analysis.d.mts +189 -0
  36. package/types/dist/lib/helpers/api_builder/analysis.d.mts.map +1 -0
  37. package/types/dist/lib/helpers/api_builder/construction.d.mts +107 -0
  38. package/types/dist/lib/helpers/api_builder/construction.d.mts.map +1 -0
  39. package/types/dist/lib/helpers/api_builder/decisions.d.mts +213 -0
  40. package/types/dist/lib/helpers/api_builder/decisions.d.mts.map +1 -0
  41. package/types/dist/lib/helpers/api_builder/metadata.d.mts +99 -0
  42. package/types/dist/lib/helpers/api_builder/metadata.d.mts.map +1 -0
  43. package/types/dist/lib/helpers/api_builder.d.mts +5 -448
  44. package/types/dist/lib/helpers/api_builder.d.mts.map +1 -1
  45. package/types/dist/lib/helpers/metadata-api.d.mts +132 -0
  46. package/types/dist/lib/helpers/metadata-api.d.mts.map +1 -0
  47. package/types/dist/lib/helpers/multidefault.d.mts.map +1 -1
  48. package/types/dist/lib/helpers/utilities.d.mts +120 -0
  49. package/types/dist/lib/helpers/utilities.d.mts.map +1 -0
  50. package/types/dist/lib/runtime/runtime-asynclocalstorage.d.mts +9 -0
  51. package/types/dist/lib/runtime/runtime-asynclocalstorage.d.mts.map +1 -1
  52. package/types/dist/lib/runtime/runtime-livebindings.d.mts +10 -0
  53. package/types/dist/lib/runtime/runtime-livebindings.d.mts.map +1 -1
  54. package/types/dist/lib/runtime/runtime.d.mts +1 -0
  55. package/types/dist/lib/runtime/runtime.d.mts.map +1 -1
  56. package/types/dist/slothlet.d.mts +0 -11
  57. package/types/dist/slothlet.d.mts.map +1 -1
  58. package/types/index.d.mts +0 -1
  59. package/API-RULES-CONDITIONS.md +0 -367
package/dist/slothlet.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2025 CLDMV/Shinrai
2
+ Copyright 2026 CLDMV/Shinrai
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -19,20 +19,23 @@
19
19
 
20
20
  import fs from "node:fs/promises";
21
21
  import path from "node:path";
22
- import { types as utilTypes } from "node:util";
23
22
  import { fileURLToPath, pathToFileURL } from "node:url";
24
23
 
25
24
  import { resolvePathFromCaller } from "@cldmv/slothlet/helpers/resolve-from-caller";
26
- import { sanitizePathName } from "@cldmv/slothlet/helpers/sanitize";
27
25
  import {
28
26
  analyzeModule,
29
27
  processModuleFromAnalysis,
30
- getCategoryBuildingDecisions,
31
- buildCategoryDecisions
28
+ buildCategoryStructure,
29
+ toapiPathKey,
30
+ shouldIncludeFile,
31
+ safeDefine,
32
+ deepMerge,
33
+ mutateLiveBindingFunction,
34
+ addApiFromFolder
32
35
  } from "@cldmv/slothlet/helpers/api_builder";
33
- import { updateInstanceData, cleanupInstance } from "./lib/helpers/instance-manager.mjs";
34
- import { disableAlsForEventEmitters, cleanupAllSlothletListeners } from "./lib/helpers/als-eventemitter.mjs";
35
- import { HookManager } from "./lib/helpers/hooks.mjs";
36
+ import { updateInstanceData, cleanupInstance } from "@cldmv/slothlet/helpers/instance-manager";
37
+ import { disableAlsForEventEmitters, cleanupAllSlothletListeners } from "@cldmv/slothlet/helpers/als-eventemitter";
38
+ import { HookManager } from "@cldmv/slothlet/helpers/hooks";
36
39
 
37
40
 
38
41
 
@@ -72,6 +75,11 @@ let DEBUG = process.argv.includes("--slothletdebug")
72
75
  : false;
73
76
 
74
77
 
78
+ if (DEBUG && !process.env.SLOTHLET_DEBUG) {
79
+ process.env.SLOTHLET_DEBUG = "1";
80
+ }
81
+
82
+
75
83
  export const self = {};
76
84
 
77
85
 
@@ -177,31 +185,18 @@ const slothletObject = {
177
185
  if (!this.modes) {
178
186
  this.modes = {};
179
187
  const modesDir = path.join(__dirname, "lib", "modes");
180
-
181
-
188
+
182
189
  const dirents = await fs.readdir(modesDir, { withFileTypes: true });
183
190
  const modeFiles = dirents.filter((d) => d.isFile()).map((d) => path.join(modesDir, d.name));
184
-
185
-
186
191
 
187
192
  for (const file of modeFiles) {
188
-
189
-
190
193
  const modePath = file;
191
194
 
192
-
193
-
194
-
195
-
196
-
197
195
  const modeName = path.parse(file).name.replace(/^slothlet_/, "");
198
196
  if (!modeName || modeName.includes(" ")) continue;
199
197
  try {
200
198
 
201
199
  const modeUrl = pathToFileURL(modePath).href;
202
-
203
-
204
-
205
200
 
206
201
  const imported = await import(modeUrl);
207
202
  if (imported && typeof imported === "object") {
@@ -213,10 +208,6 @@ const slothletObject = {
213
208
  }
214
209
  }
215
210
 
216
-
217
-
218
-
219
-
220
211
  this.mode = executionEngine;
221
212
  this.api_mode = api_mode;
222
213
  let api;
@@ -224,7 +215,7 @@ const slothletObject = {
224
215
  if (executionEngine === "singleton") {
225
216
 
226
217
 
227
- const { context = null, reference = null, sanitize = null, hooks = false, engine, mode, ...loadConfig } = options;
218
+ const { context = null, reference = null, sanitize = null, hooks = false, scope, engine, mode, ...loadConfig } = options;
228
219
  this.context = context;
229
220
  this.reference = reference;
230
221
 
@@ -252,6 +243,20 @@ const slothletObject = {
252
243
  this.hookManager = new HookManager(hooksEnabled, hooksPattern, { suppressErrors: hooksSuppressErrors });
253
244
 
254
245
 
246
+ if (scope && typeof scope === "object") {
247
+ const mergeStrategy = scope.merge || "shallow";
248
+ if (mergeStrategy !== "shallow" && mergeStrategy !== "deep") {
249
+ throw new TypeError(`Invalid scope.merge value: "${mergeStrategy}". Must be "shallow" or "deep".`);
250
+ }
251
+ this.config.scope = { merge: mergeStrategy };
252
+ } else if (scope === false) {
253
+ this.config.scope = { enabled: false };
254
+ } else {
255
+
256
+ this.config.scope = { merge: "shallow" };
257
+ }
258
+
259
+
255
260
  if (sanitize !== null) {
256
261
  this.config.sanitize = sanitize;
257
262
  }
@@ -278,9 +283,6 @@ const slothletObject = {
278
283
  await this.load(loadConfig, { context, reference });
279
284
 
280
285
 
281
-
282
-
283
-
284
286
  return this.boundapi;
285
287
  } else {
286
288
  const { createEngine } = await import("./lib/engine/slothlet_engine.mjs");
@@ -318,17 +320,10 @@ const slothletObject = {
318
320
  }
319
321
  }
320
322
 
321
-
322
-
323
323
  let apiDir = this.config.dir || "api";
324
324
 
325
325
  if (apiDir && !path.isAbsolute(apiDir)) {
326
-
327
326
  apiDir = resolvePathFromCaller(apiDir);
328
-
329
-
330
-
331
-
332
327
  }
333
328
 
334
329
  if (this.loaded) return this.api;
@@ -381,70 +376,22 @@ const slothletObject = {
381
376
  }
382
377
  }
383
378
 
384
-
385
-
386
379
  const l_ctxRef = { ...{ context: null, reference: null }, ...ctxRef };
387
380
 
388
-
389
-
390
-
391
381
  const _boundapi = this.createBoundApi(l_ctxRef.reference);
392
382
 
393
383
  mutateLiveBindingFunction(this.boundapi, _boundapi);
394
384
 
395
385
  this.updateBindings(this.context, this.reference, this.boundapi);
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
- this.loaded = true;
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
386
 
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
-
439
-
387
+ this.loaded = true;
440
388
 
441
389
  return this.boundapi;
442
390
  },
443
391
 
444
392
 
445
393
  _toapiPathKey(name) {
446
- return sanitizePathName(name, this.config.sanitize || {});
447
-
394
+ return toapiPathKey(name, this.config.sanitize || {});
448
395
  },
449
396
 
450
397
 
@@ -452,288 +399,13 @@ const slothletObject = {
452
399
  const { currentDepth = 0, maxDepth = Infinity, mode = "eager", subdirHandler } = options;
453
400
 
454
401
 
455
- const decisions = await buildCategoryDecisions(categoryPath, {
402
+ return buildCategoryStructure(categoryPath, {
456
403
  currentDepth,
457
404
  maxDepth,
458
405
  mode,
459
406
  subdirHandler,
460
407
  instance: this
461
408
  });
462
-
463
-
464
- if (decisions.type === "single-file") {
465
- const { singleFile } = decisions;
466
- const { mod, moduleName } = singleFile;
467
-
468
-
469
- if (decisions.shouldFlatten) {
470
- switch (decisions.flattenType) {
471
- case "function-folder-match":
472
- case "default-function":
473
- try {
474
- Object.defineProperty(mod, "name", { value: decisions.preferredName, configurable: true });
475
- } catch {
476
-
477
- }
478
- return mod;
479
-
480
- case "default-export-flatten":
481
-
482
- return mod;
483
-
484
- case "object-auto-flatten":
485
-
486
- return mod[decisions.preferredName];
487
-
488
- case "parent-level-flatten": {
489
-
490
- const exportValue = mod[Object.keys(mod).filter((k) => k !== "default")[0]];
491
- return { [decisions.preferredName]: exportValue };
492
- }
493
-
494
- case "filename-folder-match-flatten":
495
-
496
- return mod;
497
- }
498
- }
499
-
500
-
501
- if (decisions.preferredName && decisions.preferredName !== moduleName) {
502
- return { [decisions.preferredName]: mod };
503
- }
504
-
505
-
506
- return { [moduleName]: mod };
507
- }
508
-
509
-
510
- const categoryModules = {};
511
- const { categoryName, processedModules, subdirectoryDecisions } = decisions;
512
-
513
-
514
- for (const moduleDecision of processedModules) {
515
- const { moduleName, mod, type, apiPathKey, shouldFlatten, flattenType, specialHandling, processedExports } = moduleDecision;
516
-
517
- if (specialHandling === "category-merge") {
518
-
519
- if (
520
- Object.prototype.hasOwnProperty.call(mod, categoryName) &&
521
- typeof mod[categoryName] === "object" &&
522
- mod[categoryName] !== null
523
- ) {
524
- Object.assign(categoryModules, mod[categoryName]);
525
- for (const [key, value] of Object.entries(mod)) {
526
- if (key !== categoryName) categoryModules[this._toapiPathKey(key)] = value;
527
- }
528
- } else {
529
- Object.assign(categoryModules, mod);
530
- }
531
- } else if (type === "function") {
532
-
533
- if (specialHandling === "multi-default-filename") {
534
- try {
535
- Object.defineProperty(mod, "name", { value: moduleName, configurable: true });
536
- } catch {
537
-
538
- }
539
- categoryModules[moduleName] = mod;
540
- } else if (specialHandling === "prefer-function-name") {
541
- categoryModules[apiPathKey] = mod;
542
- } else {
543
-
544
- categoryModules[apiPathKey] = mod;
545
- }
546
- } else if (type === "self-referential") {
547
-
548
- categoryModules[moduleName] = mod[moduleName] || mod;
549
- } else if (type === "object") {
550
-
551
- if (specialHandling === "preferred-export-names") {
552
- Object.assign(categoryModules, processedExports);
553
- } else if (shouldFlatten) {
554
- switch (flattenType) {
555
- case "single-default-object": {
556
-
557
-
558
- let flattened;
559
-
560
-
561
- const defaultExport = mod.default;
562
- const hasNamedExports = Object.keys(mod).some((k) => k !== "default");
563
-
564
- if (hasNamedExports && defaultExport && typeof defaultExport === "object") {
565
-
566
- const isProxy = utilTypes?.isProxy?.(defaultExport) ?? false;
567
-
568
- if (isProxy) {
569
-
570
- flattened = defaultExport;
571
- let assignmentFailed = false;
572
-
573
- const failedMap = new Map();
574
-
575
-
576
- for (const [key, value] of Object.entries(mod)) {
577
- if (key !== "default") {
578
- try {
579
- flattened[key] = value;
580
- } catch (e) {
581
-
582
- assignmentFailed = true;
583
- failedMap.set(key, value);
584
- if (this.config?.debug) {
585
- console.warn(
586
- `Could not assign '${key}' to proxy object in module '${moduleName}' at '${categoryPath}':`,
587
- e.message
588
- );
589
- }
590
- }
591
- }
592
- }
593
-
594
-
595
- if (assignmentFailed) {
596
-
597
-
598
-
599
-
600
-
601
-
602
-
603
-
604
- const originalProxy = flattened;
605
- flattened = new Proxy(originalProxy, {
606
- get(target, prop, receiver) {
607
-
608
- if (failedMap.has(prop)) return failedMap.get(prop);
609
-
610
-
611
- return Reflect.get(target, prop, receiver);
612
- },
613
- has(target, prop) {
614
-
615
- if (failedMap.has(prop)) return true;
616
- return Reflect.has(target, prop);
617
- },
618
- ownKeys(target) {
619
- const originalKeys = Reflect.ownKeys(target);
620
- const failedKeys = Array.from(failedMap.keys());
621
- return [...new Set([...originalKeys, ...failedKeys])];
622
- },
623
- getOwnPropertyDescriptor(target, prop) {
624
- if (failedMap.has(prop)) {
625
- return { configurable: true, enumerable: true, value: failedMap.get(prop) };
626
- }
627
- return Reflect.getOwnPropertyDescriptor(target, prop);
628
- }
629
- });
630
- }
631
- } else {
632
-
633
- flattened = { ...defaultExport };
634
- for (const [key, value] of Object.entries(mod)) {
635
- if (key !== "default") {
636
- flattened[key] = value;
637
- }
638
- }
639
- }
640
- } else {
641
-
642
- flattened = defaultExport;
643
- }
644
-
645
- categoryModules[apiPathKey] = flattened;
646
- break;
647
- }
648
- case "multi-default-no-default": {
649
-
650
- const moduleKeys = Object.keys(mod).filter((k) => k !== "default");
651
- for (const key of moduleKeys) {
652
- categoryModules[key] = mod[key];
653
- }
654
- break;
655
- }
656
- case "single-named-export-match":
657
-
658
- categoryModules[apiPathKey] = mod[apiPathKey];
659
- break;
660
- case "category-name-match-flatten": {
661
-
662
- const moduleKeys = Object.keys(mod).filter((k) => k !== "default");
663
- for (const key of moduleKeys) {
664
- categoryModules[key] = mod[key];
665
- }
666
- break;
667
- }
668
- }
669
- } else {
670
-
671
- categoryModules[apiPathKey] = mod;
672
- }
673
- }
674
- }
675
-
676
-
677
- for (const subDirDecision of subdirectoryDecisions) {
678
- if (subDirDecision.shouldRecurse) {
679
- const { name, path: subDirPath, apiPathKey } = subDirDecision;
680
- let subModule;
681
-
682
- if (mode === "lazy" && typeof subdirHandler === "function") {
683
- subModule = subdirHandler({
684
- subDirEntry: { name },
685
- subDirPath,
686
- key: apiPathKey,
687
- categoryModules,
688
- currentDepth,
689
- maxDepth
690
- });
691
- } else {
692
- subModule = await this._buildCategory(subDirPath, {
693
- currentDepth: currentDepth + 1,
694
- maxDepth,
695
- mode: "eager"
696
- });
697
- }
698
-
699
-
700
-
701
- if (
702
- typeof subModule === "function" &&
703
- subModule.name &&
704
- subModule.name.toLowerCase() === apiPathKey.toLowerCase() &&
705
- subModule.name !== apiPathKey
706
- ) {
707
-
708
- categoryModules[subModule.name] = subModule;
709
- } else {
710
- categoryModules[apiPathKey] = subModule;
711
- }
712
- }
713
- }
714
-
715
-
716
- const keys = Object.keys(categoryModules);
717
- if (keys.length === 1) {
718
- const singleKey = keys[0];
719
- if (singleKey === categoryName) {
720
- const single = categoryModules[singleKey];
721
- if (typeof single === "function") {
722
- if (single.name !== categoryName) {
723
- try {
724
- Object.defineProperty(single, "name", { value: categoryName, configurable: true });
725
- } catch {
726
-
727
- }
728
- }
729
- return single;
730
- } else if (single && typeof single === "object" && !Array.isArray(single)) {
731
- return single;
732
- }
733
- }
734
- }
735
-
736
- return categoryModules;
737
409
  },
738
410
 
739
411
 
@@ -763,117 +435,8 @@ const slothletObject = {
763
435
  },
764
436
 
765
437
 
766
- async _buildCategoryEnhanced(categoryPath, options = {}) {
767
- const { currentDepth = 0, maxDepth = Infinity, mode = "eager", subdirHandler } = options;
768
-
769
-
770
- const decisions = await getCategoryBuildingDecisions(categoryPath, {
771
- instance: this,
772
- currentDepth,
773
- maxDepth,
774
- debug: this.config.debug
775
- });
776
-
777
- const { processingStrategy, categoryName, processedModules, subDirectories } = decisions;
778
-
779
-
780
- if (processingStrategy === "single-file" && processedModules.length === 1) {
781
- const { processedModule, flattening } = processedModules[0];
782
-
783
- if (flattening.shouldFlatten) {
784
-
785
- if (typeof processedModule === "function") {
786
- try {
787
- Object.defineProperty(processedModule, "name", { value: flattening.apiPathKey, configurable: true });
788
- } catch {
789
-
790
- }
791
- }
792
- return processedModule;
793
- }
794
-
795
-
796
- return { [flattening.apiPathKey]: processedModule };
797
- }
798
-
799
-
800
- const categoryModules = {};
801
-
802
- for (const { processedModule, flattening } of processedModules) {
803
- categoryModules[flattening.apiPathKey] = processedModule;
804
- }
805
-
806
-
807
- for (const { dirEntry: subDirEntry, apiPathKey: key } of subDirectories) {
808
- const subDirPath = path.join(categoryPath, subDirEntry.name);
809
- let subModule;
810
-
811
- if (mode === "lazy" && typeof subdirHandler === "function") {
812
- subModule = subdirHandler({
813
- subDirEntry,
814
- subDirPath,
815
- key,
816
- categoryModules,
817
- currentDepth,
818
- maxDepth
819
- });
820
- } else {
821
-
822
- subModule = await this._buildCategoryEnhanced(subDirPath, {
823
- currentDepth: currentDepth + 1,
824
- maxDepth,
825
- mode: "eager"
826
- });
827
- }
828
-
829
-
830
- if (
831
- typeof subModule === "function" &&
832
- subModule.name &&
833
- subModule.name.toLowerCase() === key.toLowerCase() &&
834
- subModule.name !== key
835
- ) {
836
- categoryModules[subModule.name] = subModule;
837
- } else {
838
- categoryModules[key] = subModule;
839
- }
840
- }
841
-
842
-
843
- const keys = Object.keys(categoryModules);
844
- if (keys.length === 1) {
845
- const singleKey = keys[0];
846
- if (singleKey === categoryName) {
847
- const single = categoryModules[singleKey];
848
- if (typeof single === "function") {
849
- if (single.name !== categoryName) {
850
- try {
851
- Object.defineProperty(single, "name", { value: categoryName, configurable: true });
852
- } catch {
853
-
854
- }
855
- }
856
- return single;
857
- } else if (single && typeof single === "object" && !Array.isArray(single)) {
858
- return single;
859
- }
860
- }
861
- }
862
-
863
- return categoryModules;
864
- },
865
-
866
-
867
438
  _shouldIncludeFile(entry) {
868
-
869
- if (!entry.isFile()) return false;
870
-
871
- if (!(entry.name.endsWith(".mjs") || entry.name.endsWith(".cjs") || entry.name.endsWith(".js"))) return false;
872
-
873
- if (entry.name.startsWith(".")) return false;
874
-
875
- if (entry.name.startsWith("__slothlet_")) return false;
876
- return true;
439
+ return shouldIncludeFile(entry);
877
440
  },
878
441
 
879
442
 
@@ -956,11 +519,7 @@ const slothletObject = {
956
519
  createBoundApi(ref = null) {
957
520
  if (!this.api) throw new Error("BindleApi modules not loaded. Call load() first.");
958
521
 
959
-
960
-
961
-
962
522
  let boundApi;
963
-
964
523
 
965
524
  boundApi = this.api;
966
525
 
@@ -978,24 +537,6 @@ const slothletObject = {
978
537
  }
979
538
 
980
539
 
981
-
982
-
983
-
984
-
985
-
986
-
987
-
988
-
989
-
990
-
991
-
992
-
993
-
994
-
995
-
996
-
997
-
998
-
999
540
  const instance = this;
1000
541
  this.safeDefine(boundApi, "describe", function (showAll = false) {
1001
542
 
@@ -1085,6 +626,43 @@ const slothletObject = {
1085
626
  } else if (this.config && this.config.debug) {
1086
627
  console.warn("Could not redefine boundApi.addApi: not configurable");
1087
628
  }
629
+
630
+
631
+ const runDesc = Object.getOwnPropertyDescriptor(boundApi, "run");
632
+ if (!runDesc || runDesc.configurable) {
633
+ Object.defineProperty(boundApi, "run", {
634
+ value: this.run.bind(this),
635
+ writable: true,
636
+ configurable: true,
637
+ enumerable: false
638
+ });
639
+ } else if (this.config && this.config.debug) {
640
+ console.warn("Could not redefine boundApi.run: not configurable");
641
+ }
642
+
643
+
644
+ const instanceIdDesc = Object.getOwnPropertyDescriptor(boundApi, "instanceId");
645
+ if (!instanceIdDesc || instanceIdDesc.configurable) {
646
+ Object.defineProperty(boundApi, "instanceId", {
647
+ value: this.instanceId,
648
+ writable: false,
649
+ configurable: true,
650
+ enumerable: false
651
+ });
652
+ }
653
+
654
+
655
+ const scopeDesc = Object.getOwnPropertyDescriptor(boundApi, "scope");
656
+ if (!scopeDesc || scopeDesc.configurable) {
657
+ Object.defineProperty(boundApi, "scope", {
658
+ value: this.scope.bind(this),
659
+ writable: true,
660
+ configurable: true,
661
+ enumerable: false
662
+ });
663
+ } else if (this.config && this.config.debug) {
664
+ console.warn("Could not redefine boundApi.scope: not configurable");
665
+ }
1088
666
 
1089
667
 
1090
668
 
@@ -1097,24 +675,7 @@ const slothletObject = {
1097
675
 
1098
676
 
1099
677
  safeDefine(obj, key, value, enumerable = false) {
1100
- const desc = Object.getOwnPropertyDescriptor(obj, key);
1101
- if (!desc) {
1102
- Object.defineProperty(obj, key, {
1103
- value,
1104
- writable: true,
1105
- configurable: true,
1106
- enumerable
1107
- });
1108
- } else if (desc.configurable) {
1109
- Object.defineProperty(obj, key, {
1110
- value,
1111
- writable: true,
1112
- configurable: true,
1113
- enumerable
1114
- });
1115
- } else if (this.config && this.config.debug) {
1116
- console.warn(`Could not redefine boundApi.${key}: not configurable`);
1117
- }
678
+ return safeDefine(obj, key, value, enumerable, this.config);
1118
679
  },
1119
680
 
1120
681
 
@@ -1133,220 +694,121 @@ const slothletObject = {
1133
694
  },
1134
695
 
1135
696
 
1136
- async addApi(apiPath, folderPath) {
1137
- if (!this.loaded) {
1138
- throw new Error("[slothlet] Cannot add API: API not loaded. Call create() or load() first.");
1139
- }
697
+ async addApi(apiPath, folderPath, metadata = {}) {
698
+ return addApiFromFolder({ apiPath, folderPath, instance: this, metadata });
699
+ },
1140
700
 
1141
-
1142
- if (typeof apiPath !== "string") {
1143
- throw new TypeError("[slothlet] addApi: 'apiPath' must be a string.");
1144
- }
1145
- const normalizedApiPath = apiPath.trim();
1146
- if (normalizedApiPath === "") {
1147
- throw new TypeError("[slothlet] addApi: 'apiPath' must be a non-empty, non-whitespace string.");
1148
- }
1149
- const pathParts = normalizedApiPath.split(".");
1150
- if (pathParts.some((part) => part === "")) {
1151
- throw new Error(`[slothlet] addApi: 'apiPath' must not contain empty segments. Received: "${normalizedApiPath}"`);
1152
- }
701
+
1153
702
 
1154
-
1155
- if (typeof folderPath !== "string") {
1156
- throw new TypeError("[slothlet] addApi: 'folderPath' must be a string.");
703
+
704
+ run(contextData, callback, ...args) {
705
+ if (this.config.scope?.enabled === false) {
706
+ throw new Error("Per-request context (scope) is disabled for this instance.");
1157
707
  }
1158
708
 
1159
-
1160
- let resolvedFolderPath = folderPath;
1161
- if (!path.isAbsolute(folderPath)) {
1162
- resolvedFolderPath = resolvePathFromCaller(folderPath);
709
+ if (typeof callback !== "function") {
710
+ throw new TypeError("Callback must be a function.");
1163
711
  }
1164
712
 
1165
-
1166
- let stats;
1167
- try {
1168
- stats = await fs.stat(resolvedFolderPath);
1169
- } catch (error) {
1170
- throw new Error(`[slothlet] addApi: Cannot access folder: ${resolvedFolderPath} - ${error.message}`);
1171
- }
1172
- if (!stats.isDirectory()) {
1173
- throw new Error(`[slothlet] addApi: Path is not a directory: ${resolvedFolderPath}`);
1174
- }
713
+ const runtimeType = this.config.runtime || "async";
714
+ let requestALS;
715
+ if (runtimeType === "async") {
716
+ return import("@cldmv/slothlet/runtime/async").then((asyncRuntime) => {
717
+ requestALS = asyncRuntime.requestALS;
718
+ const parentContext = requestALS.getStore() || {};
1175
719
 
1176
- if (this.config.debug) {
1177
- console.log(`[DEBUG] addApi: Loading modules from ${resolvedFolderPath} to path: ${normalizedApiPath}`);
1178
- }
720
+ let mergedContext;
721
+ if (this.config.scope?.merge === "deep") {
722
+ const instanceContext = this.context || {};
723
+ let temp = this._deepMerge({}, instanceContext);
724
+ temp = this._deepMerge(temp, parentContext);
725
+ mergedContext = this._deepMerge(temp, contextData);
726
+ } else {
727
+ mergedContext = { ...parentContext, ...contextData };
728
+ }
1179
729
 
1180
-
1181
- let newModules;
1182
- if (this.config.lazy) {
1183
-
1184
- newModules = await this.modes.lazy.create.call(this, resolvedFolderPath, this.config.apiDepth || Infinity, 0);
730
+ return requestALS.run(mergedContext, () => callback(...args));
731
+ });
1185
732
  } else {
1186
-
1187
- newModules = await this.modes.eager.create.call(this, resolvedFolderPath, this.config.apiDepth || Infinity, 0);
1188
- }
1189
-
1190
- if (this.config.debug) {
1191
- if (newModules && typeof newModules === "object") {
1192
- console.log(`[DEBUG] addApi: Loaded modules:`, Object.keys(newModules));
1193
- } else {
1194
- console.log(
1195
- `[DEBUG] addApi: Loaded modules (non-object):`,
1196
- typeof newModules === "function" ? `[Function: ${newModules.name || "anonymous"}]` : newModules
1197
- );
1198
- }
1199
- }
1200
-
1201
-
1202
- let currentTarget = this.api;
1203
- let currentBoundTarget = this.boundapi;
1204
-
1205
- for (let i = 0; i < pathParts.length - 1; i++) {
1206
- const part = pathParts[i];
1207
- const key = this._toapiPathKey(part);
1208
-
1209
-
1210
-
1211
-
1212
- if (Object.prototype.hasOwnProperty.call(currentTarget, key)) {
1213
- const existing = currentTarget[key];
1214
- if (existing === null || (typeof existing !== "object" && typeof existing !== "function")) {
1215
- throw new Error(
1216
- `[slothlet] Cannot extend API path "${normalizedApiPath}" through segment "${part}": ` +
1217
- `existing value is type "${typeof existing}", cannot add properties.`
1218
- );
1219
- }
1220
-
1221
-
1222
- } else {
1223
- currentTarget[key] = {};
1224
- }
1225
- if (Object.prototype.hasOwnProperty.call(currentBoundTarget, key)) {
1226
- const existingBound = currentBoundTarget[key];
1227
- if (existingBound === null || (typeof existingBound !== "object" && typeof existingBound !== "function")) {
1228
- throw new Error(
1229
- `[slothlet] Cannot extend bound API path "${normalizedApiPath}" through segment "${part}": ` +
1230
- `existing value is type "${typeof existingBound}", cannot add properties.`
1231
- );
733
+ return import("@cldmv/slothlet/runtime/live").then((liveRuntime) => {
734
+ requestALS = liveRuntime.requestALS;
735
+ const parentContext = requestALS.getStore() || {};
736
+
737
+ let mergedContext;
738
+ if (this.config.scope?.merge === "deep") {
739
+ const instanceContext = this.context || {};
740
+ let temp = this._deepMerge({}, instanceContext);
741
+ temp = this._deepMerge(temp, parentContext);
742
+ mergedContext = this._deepMerge(temp, contextData);
743
+ } else {
744
+ mergedContext = { ...parentContext, ...contextData };
1232
745
  }
1233
-
1234
- } else {
1235
- currentBoundTarget[key] = {};
1236
- }
1237
746
 
1238
-
1239
- currentTarget = currentTarget[key];
1240
- currentBoundTarget = currentBoundTarget[key];
747
+ return requestALS.run(mergedContext, () => callback(...args));
748
+ });
1241
749
  }
750
+ },
1242
751
 
1243
-
1244
- const finalKey = this._toapiPathKey(pathParts[pathParts.length - 1]);
1245
-
1246
-
1247
- if (typeof newModules === "function") {
1248
-
1249
-
1250
- if (Object.prototype.hasOwnProperty.call(currentTarget, finalKey)) {
1251
- const existing = currentTarget[finalKey];
752
+
753
+ scope({ context, fn, args }) {
754
+ if (this.config.scope?.enabled === false) {
755
+ throw new Error("Per-request context (scope) is disabled for this instance.");
756
+ }
1252
757
 
1253
-
1254
- if (this.config.allowApiOverwrite === false) {
1255
- console.warn(
1256
- `[slothlet] Skipping addApi: API path "${normalizedApiPath}" final key "${finalKey}" ` +
1257
- `already exists (type: "${typeof existing}"). Set allowApiOverwrite: true to allow overwrites.`
1258
- );
1259
- return;
1260
- }
758
+ if (!context || typeof context !== "object") {
759
+ throw new TypeError("context must be an object.");
760
+ }
1261
761
 
1262
-
1263
- if (existing !== null && typeof existing !== "function") {
1264
- console.warn(
1265
- `[slothlet] Overwriting existing non-function value at API path "${normalizedApiPath}" ` +
1266
- `final key "${finalKey}" with a function. Previous type: "${typeof existing}".`
1267
- );
1268
- } else if (typeof existing === "function") {
1269
-
1270
- console.warn(
1271
- `[slothlet] Overwriting existing function at API path "${normalizedApiPath}" ` + `final key "${finalKey}" with a new function.`
1272
- );
1273
- }
1274
- }
1275
- currentTarget[finalKey] = newModules;
1276
- currentBoundTarget[finalKey] = newModules;
1277
- } else if (typeof newModules === "object" && newModules !== null) {
1278
-
1279
- if (Object.prototype.hasOwnProperty.call(currentTarget, finalKey)) {
1280
- const existing = currentTarget[finalKey];
762
+ if (typeof fn !== "function") {
763
+ throw new TypeError("fn must be a function.");
764
+ }
1281
765
 
1282
-
1283
- if (this.config.allowApiOverwrite === false && existing !== undefined && existing !== null) {
1284
-
1285
- const hasContent = typeof existing === "object" ? Object.keys(existing).length > 0 : true;
1286
- if (hasContent) {
1287
- console.warn(
1288
- `[slothlet] Skipping addApi merge: API path "${normalizedApiPath}" final key "${finalKey}" ` +
1289
- `already exists with content (type: "${typeof existing}"). Set allowApiOverwrite: true to allow merging.`
1290
- );
1291
- return;
1292
- }
1293
- }
766
+ const runtimeType = this.config.runtime || "async";
767
+ let requestALS;
768
+ if (runtimeType === "async") {
769
+ return import("./lib/runtime/runtime-asynclocalstorage.mjs").then((asyncRuntime) => {
770
+ requestALS = asyncRuntime.requestALS;
771
+ const parentContext = requestALS.getStore() || {};
1294
772
 
1295
- if (existing !== null && typeof existing !== "object" && typeof existing !== "function") {
1296
- throw new Error(
1297
- `[slothlet] Cannot merge API at "${normalizedApiPath}": ` +
1298
- `existing value at final key "${finalKey}" is type "${typeof existing}", cannot merge into primitives.`
1299
- );
1300
- }
1301
- }
1302
- if (Object.prototype.hasOwnProperty.call(currentBoundTarget, finalKey)) {
1303
- const existingBound = currentBoundTarget[finalKey];
1304
- if (existingBound !== null && typeof existingBound !== "object" && typeof existingBound !== "function") {
1305
- throw new Error(
1306
- `[slothlet] Cannot merge bound API at "${normalizedApiPath}": ` +
1307
- `existing value at final key "${finalKey}" is type "${typeof existingBound}", cannot merge into primitives.`
1308
- );
773
+ let mergedContext;
774
+ if (this.config.scope?.merge === "deep") {
775
+ const instanceContext = this.context || {};
776
+ let temp = this._deepMerge({}, instanceContext);
777
+ temp = this._deepMerge(temp, parentContext);
778
+ mergedContext = this._deepMerge(temp, context);
779
+ } else {
780
+ mergedContext = { ...parentContext, ...context };
1309
781
  }
1310
- }
1311
782
 
1312
-
1313
- if (!currentTarget[finalKey]) {
1314
- currentTarget[finalKey] = {};
1315
- }
1316
- if (!currentBoundTarget[finalKey]) {
1317
- currentBoundTarget[finalKey] = {};
1318
- }
1319
-
1320
-
1321
-
1322
-
1323
-
1324
-
1325
- Object.assign(currentTarget[finalKey], newModules);
1326
- Object.assign(currentBoundTarget[finalKey], newModules);
1327
- } else if (newModules === null || newModules === undefined) {
1328
-
1329
- const receivedType = newModules === null ? "null" : "undefined";
1330
- console.warn(
1331
- `[slothlet] addApi: No modules loaded from folder at API path "${normalizedApiPath}". ` +
1332
- `Loaded modules resulted in ${receivedType}. Check that the folder contains valid module files.`
1333
- );
783
+ const argsArray = args || [];
784
+ return requestALS.run(mergedContext, () => fn(...argsArray));
785
+ });
1334
786
  } else {
1335
-
1336
-
1337
- currentTarget[finalKey] = newModules;
1338
- currentBoundTarget[finalKey] = newModules;
1339
- }
1340
-
1341
-
1342
- this.updateBindings(this.context, this.reference, this.boundapi);
787
+ return import("./lib/runtime/runtime-livebindings.mjs").then((liveRuntime) => {
788
+ requestALS = liveRuntime.requestALS;
789
+ const parentContext = requestALS.getStore() || {};
790
+
791
+ let mergedContext;
792
+ if (this.config.scope?.merge === "deep") {
793
+ const instanceContext = this.context || {};
794
+ let temp = this._deepMerge({}, instanceContext);
795
+ temp = this._deepMerge(temp, parentContext);
796
+ mergedContext = this._deepMerge(temp, context);
797
+ } else {
798
+ mergedContext = { ...parentContext, ...context };
799
+ }
1343
800
 
1344
- if (this.config.debug) {
1345
- console.log(`[DEBUG] addApi: Successfully added modules at ${normalizedApiPath}`);
801
+ const argsArray = args || [];
802
+ return requestALS.run(mergedContext, () => fn(...argsArray));
803
+ });
1346
804
  }
1347
805
  },
1348
806
 
1349
807
 
808
+ _deepMerge(target, source) {
809
+ return deepMerge(target, source);
810
+ },
811
+
1350
812
  async shutdown() {
1351
813
 
1352
814
 
@@ -1423,52 +885,6 @@ const slothletObject = {
1423
885
  };
1424
886
 
1425
887
 
1426
- export function mutateLiveBindingFunction(target, source) {
1427
- if (typeof source === "function") {
1428
- target._impl = (...args) => source(...args);
1429
-
1430
- for (const key of Object.keys(target)) {
1431
- if (key !== "_impl" && key !== "__ctx") delete target[key];
1432
- }
1433
-
1434
- for (const key of Object.getOwnPropertyNames(source)) {
1435
- if (key !== "length" && key !== "name" && key !== "prototype" && key !== "_impl" && key !== "__ctx") {
1436
- try {
1437
- target[key] = source[key];
1438
- } catch {
1439
-
1440
- }
1441
- }
1442
- }
1443
- } else if (typeof source === "object" && source !== null) {
1444
-
1445
- for (const key of Object.keys(target)) {
1446
- if (key !== "_impl" && key !== "__ctx") delete target[key];
1447
- }
1448
-
1449
- for (const [key, value] of Object.entries(source)) {
1450
- if (key !== "__ctx") {
1451
- target[key] = value;
1452
- }
1453
- }
1454
-
1455
- const managementMethods = ["shutdown", "addApi", "describe"];
1456
- for (const method of managementMethods) {
1457
- const desc = Object.getOwnPropertyDescriptor(source, method);
1458
- if (desc) {
1459
- try {
1460
- Object.defineProperty(target, method, desc);
1461
- } catch {
1462
-
1463
- }
1464
- }
1465
- }
1466
-
1467
- if (typeof source._impl === "function") {
1468
- target._impl = source._impl;
1469
- }
1470
- }
1471
- }
1472
888
 
1473
889
  export { slothlet };
1474
890
  export default slothlet;