@cldmv/slothlet 2.8.0 → 2.9.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 (37) hide show
  1. package/AGENT-USAGE.md +1 -1
  2. package/README.md +253 -1578
  3. package/dist/lib/helpers/als-eventemitter.mjs +4 -5
  4. package/dist/lib/helpers/api_builder/add_api.mjs +237 -0
  5. package/dist/lib/helpers/api_builder/analysis.mjs +522 -0
  6. package/dist/lib/helpers/api_builder/construction.mjs +457 -0
  7. package/dist/lib/helpers/api_builder/decisions.mjs +737 -0
  8. package/dist/lib/helpers/api_builder.mjs +16 -1567
  9. package/dist/lib/helpers/utilities.mjs +121 -0
  10. package/dist/lib/runtime/runtime-asynclocalstorage.mjs +44 -17
  11. package/dist/lib/runtime/runtime-livebindings.mjs +18 -3
  12. package/dist/lib/runtime/runtime.mjs +3 -3
  13. package/dist/slothlet.mjs +146 -746
  14. package/docs/API-RULES-CONDITIONS.md +508 -0
  15. package/{API-RULES.md → docs/API-RULES.md} +127 -72
  16. package/package.json +11 -9
  17. package/types/dist/lib/helpers/als-eventemitter.d.mts.map +1 -1
  18. package/types/dist/lib/helpers/api_builder/add_api.d.mts +60 -0
  19. package/types/dist/lib/helpers/api_builder/add_api.d.mts.map +1 -0
  20. package/types/dist/lib/helpers/api_builder/analysis.d.mts +189 -0
  21. package/types/dist/lib/helpers/api_builder/analysis.d.mts.map +1 -0
  22. package/types/dist/lib/helpers/api_builder/construction.d.mts +107 -0
  23. package/types/dist/lib/helpers/api_builder/construction.d.mts.map +1 -0
  24. package/types/dist/lib/helpers/api_builder/decisions.d.mts +213 -0
  25. package/types/dist/lib/helpers/api_builder/decisions.d.mts.map +1 -0
  26. package/types/dist/lib/helpers/api_builder.d.mts +5 -448
  27. package/types/dist/lib/helpers/api_builder.d.mts.map +1 -1
  28. package/types/dist/lib/helpers/utilities.d.mts +120 -0
  29. package/types/dist/lib/helpers/utilities.d.mts.map +1 -0
  30. package/types/dist/lib/runtime/runtime-asynclocalstorage.d.mts +7 -0
  31. package/types/dist/lib/runtime/runtime-asynclocalstorage.d.mts.map +1 -1
  32. package/types/dist/lib/runtime/runtime-livebindings.d.mts +8 -0
  33. package/types/dist/lib/runtime/runtime-livebindings.d.mts.map +1 -1
  34. package/types/dist/slothlet.d.mts +0 -11
  35. package/types/dist/slothlet.d.mts.map +1 -1
  36. package/types/index.d.mts +0 -1
  37. package/API-RULES-CONDITIONS.md +0 -367
package/dist/slothlet.mjs CHANGED
@@ -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
 
@@ -177,31 +180,18 @@ const slothletObject = {
177
180
  if (!this.modes) {
178
181
  this.modes = {};
179
182
  const modesDir = path.join(__dirname, "lib", "modes");
180
-
181
-
183
+
182
184
  const dirents = await fs.readdir(modesDir, { withFileTypes: true });
183
185
  const modeFiles = dirents.filter((d) => d.isFile()).map((d) => path.join(modesDir, d.name));
184
-
185
-
186
186
 
187
187
  for (const file of modeFiles) {
188
-
189
-
190
188
  const modePath = file;
191
189
 
192
-
193
-
194
-
195
-
196
-
197
190
  const modeName = path.parse(file).name.replace(/^slothlet_/, "");
198
191
  if (!modeName || modeName.includes(" ")) continue;
199
192
  try {
200
193
 
201
194
  const modeUrl = pathToFileURL(modePath).href;
202
-
203
-
204
-
205
195
 
206
196
  const imported = await import(modeUrl);
207
197
  if (imported && typeof imported === "object") {
@@ -213,10 +203,6 @@ const slothletObject = {
213
203
  }
214
204
  }
215
205
 
216
-
217
-
218
-
219
-
220
206
  this.mode = executionEngine;
221
207
  this.api_mode = api_mode;
222
208
  let api;
@@ -224,7 +210,7 @@ const slothletObject = {
224
210
  if (executionEngine === "singleton") {
225
211
 
226
212
 
227
- const { context = null, reference = null, sanitize = null, hooks = false, engine, mode, ...loadConfig } = options;
213
+ const { context = null, reference = null, sanitize = null, hooks = false, scope, engine, mode, ...loadConfig } = options;
228
214
  this.context = context;
229
215
  this.reference = reference;
230
216
 
@@ -252,6 +238,20 @@ const slothletObject = {
252
238
  this.hookManager = new HookManager(hooksEnabled, hooksPattern, { suppressErrors: hooksSuppressErrors });
253
239
 
254
240
 
241
+ if (scope && typeof scope === "object") {
242
+ const mergeStrategy = scope.merge || "shallow";
243
+ if (mergeStrategy !== "shallow" && mergeStrategy !== "deep") {
244
+ throw new TypeError(`Invalid scope.merge value: "${mergeStrategy}". Must be "shallow" or "deep".`);
245
+ }
246
+ this.config.scope = { merge: mergeStrategy };
247
+ } else if (scope === false) {
248
+ this.config.scope = { enabled: false };
249
+ } else {
250
+
251
+ this.config.scope = { merge: "shallow" };
252
+ }
253
+
254
+
255
255
  if (sanitize !== null) {
256
256
  this.config.sanitize = sanitize;
257
257
  }
@@ -278,9 +278,6 @@ const slothletObject = {
278
278
  await this.load(loadConfig, { context, reference });
279
279
 
280
280
 
281
-
282
-
283
-
284
281
  return this.boundapi;
285
282
  } else {
286
283
  const { createEngine } = await import("./lib/engine/slothlet_engine.mjs");
@@ -318,17 +315,10 @@ const slothletObject = {
318
315
  }
319
316
  }
320
317
 
321
-
322
-
323
318
  let apiDir = this.config.dir || "api";
324
319
 
325
320
  if (apiDir && !path.isAbsolute(apiDir)) {
326
-
327
321
  apiDir = resolvePathFromCaller(apiDir);
328
-
329
-
330
-
331
-
332
322
  }
333
323
 
334
324
  if (this.loaded) return this.api;
@@ -381,70 +371,22 @@ const slothletObject = {
381
371
  }
382
372
  }
383
373
 
384
-
385
-
386
374
  const l_ctxRef = { ...{ context: null, reference: null }, ...ctxRef };
387
375
 
388
-
389
-
390
-
391
376
  const _boundapi = this.createBoundApi(l_ctxRef.reference);
392
377
 
393
378
  mutateLiveBindingFunction(this.boundapi, _boundapi);
394
379
 
395
380
  this.updateBindings(this.context, this.reference, this.boundapi);
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
- this.loaded = true;
405
381
 
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
-
439
-
382
+ this.loaded = true;
440
383
 
441
384
  return this.boundapi;
442
385
  },
443
386
 
444
387
 
445
388
  _toapiPathKey(name) {
446
- return sanitizePathName(name, this.config.sanitize || {});
447
-
389
+ return toapiPathKey(name, this.config.sanitize || {});
448
390
  },
449
391
 
450
392
 
@@ -452,288 +394,13 @@ const slothletObject = {
452
394
  const { currentDepth = 0, maxDepth = Infinity, mode = "eager", subdirHandler } = options;
453
395
 
454
396
 
455
- const decisions = await buildCategoryDecisions(categoryPath, {
397
+ return buildCategoryStructure(categoryPath, {
456
398
  currentDepth,
457
399
  maxDepth,
458
400
  mode,
459
401
  subdirHandler,
460
402
  instance: this
461
403
  });
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
404
  },
738
405
 
739
406
 
@@ -763,117 +430,8 @@ const slothletObject = {
763
430
  },
764
431
 
765
432
 
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
433
  _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;
434
+ return shouldIncludeFile(entry);
877
435
  },
878
436
 
879
437
 
@@ -956,11 +514,7 @@ const slothletObject = {
956
514
  createBoundApi(ref = null) {
957
515
  if (!this.api) throw new Error("BindleApi modules not loaded. Call load() first.");
958
516
 
959
-
960
-
961
-
962
517
  let boundApi;
963
-
964
518
 
965
519
  boundApi = this.api;
966
520
 
@@ -978,24 +532,6 @@ const slothletObject = {
978
532
  }
979
533
 
980
534
 
981
-
982
-
983
-
984
-
985
-
986
-
987
-
988
-
989
-
990
-
991
-
992
-
993
-
994
-
995
-
996
-
997
-
998
-
999
535
  const instance = this;
1000
536
  this.safeDefine(boundApi, "describe", function (showAll = false) {
1001
537
 
@@ -1085,6 +621,32 @@ const slothletObject = {
1085
621
  } else if (this.config && this.config.debug) {
1086
622
  console.warn("Could not redefine boundApi.addApi: not configurable");
1087
623
  }
624
+
625
+
626
+ const runDesc = Object.getOwnPropertyDescriptor(boundApi, "run");
627
+ if (!runDesc || runDesc.configurable) {
628
+ Object.defineProperty(boundApi, "run", {
629
+ value: this.run.bind(this),
630
+ writable: true,
631
+ configurable: true,
632
+ enumerable: false
633
+ });
634
+ } else if (this.config && this.config.debug) {
635
+ console.warn("Could not redefine boundApi.run: not configurable");
636
+ }
637
+
638
+
639
+ const scopeDesc = Object.getOwnPropertyDescriptor(boundApi, "scope");
640
+ if (!scopeDesc || scopeDesc.configurable) {
641
+ Object.defineProperty(boundApi, "scope", {
642
+ value: this.scope.bind(this),
643
+ writable: true,
644
+ configurable: true,
645
+ enumerable: false
646
+ });
647
+ } else if (this.config && this.config.debug) {
648
+ console.warn("Could not redefine boundApi.scope: not configurable");
649
+ }
1088
650
 
1089
651
 
1090
652
 
@@ -1097,24 +659,7 @@ const slothletObject = {
1097
659
 
1098
660
 
1099
661
  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
- }
662
+ return safeDefine(obj, key, value, enumerable, this.config);
1118
663
  },
1119
664
 
1120
665
 
@@ -1134,219 +679,120 @@ const slothletObject = {
1134
679
 
1135
680
 
1136
681
  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
- }
682
+ return addApiFromFolder({ apiPath, folderPath, instance: this });
683
+ },
1140
684
 
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
- }
685
+
1153
686
 
1154
-
1155
- if (typeof folderPath !== "string") {
1156
- throw new TypeError("[slothlet] addApi: 'folderPath' must be a string.");
687
+
688
+ run(contextData, callback, ...args) {
689
+ if (this.config.scope?.enabled === false) {
690
+ throw new Error("Per-request context (scope) is disabled for this instance.");
1157
691
  }
1158
692
 
1159
-
1160
- let resolvedFolderPath = folderPath;
1161
- if (!path.isAbsolute(folderPath)) {
1162
- resolvedFolderPath = resolvePathFromCaller(folderPath);
693
+ if (typeof callback !== "function") {
694
+ throw new TypeError("Callback must be a function.");
1163
695
  }
1164
696
 
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
- }
697
+ const runtimeType = this.config.runtime || "async";
698
+ let requestALS;
699
+ if (runtimeType === "async") {
700
+ return import("@cldmv/slothlet/runtime/async").then((asyncRuntime) => {
701
+ requestALS = asyncRuntime.requestALS;
702
+ const parentContext = requestALS.getStore() || {};
1175
703
 
1176
- if (this.config.debug) {
1177
- console.log(`[DEBUG] addApi: Loading modules from ${resolvedFolderPath} to path: ${normalizedApiPath}`);
1178
- }
704
+ let mergedContext;
705
+ if (this.config.scope?.merge === "deep") {
706
+ const instanceContext = this.context || {};
707
+ let temp = this._deepMerge({}, instanceContext);
708
+ temp = this._deepMerge(temp, parentContext);
709
+ mergedContext = this._deepMerge(temp, contextData);
710
+ } else {
711
+ mergedContext = { ...parentContext, ...contextData };
712
+ }
1179
713
 
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);
714
+ return requestALS.run(mergedContext, () => callback(...args));
715
+ });
1185
716
  } 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
- );
717
+ return import("@cldmv/slothlet/runtime/live").then((liveRuntime) => {
718
+ requestALS = liveRuntime.requestALS;
719
+ const parentContext = requestALS.getStore() || {};
720
+
721
+ let mergedContext;
722
+ if (this.config.scope?.merge === "deep") {
723
+ const instanceContext = this.context || {};
724
+ let temp = this._deepMerge({}, instanceContext);
725
+ temp = this._deepMerge(temp, parentContext);
726
+ mergedContext = this._deepMerge(temp, contextData);
727
+ } else {
728
+ mergedContext = { ...parentContext, ...contextData };
1232
729
  }
1233
-
1234
- } else {
1235
- currentBoundTarget[key] = {};
1236
- }
1237
730
 
1238
-
1239
- currentTarget = currentTarget[key];
1240
- currentBoundTarget = currentBoundTarget[key];
731
+ return requestALS.run(mergedContext, () => callback(...args));
732
+ });
1241
733
  }
734
+ },
1242
735
 
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];
736
+
737
+ scope({ context, fn, args }) {
738
+ if (this.config.scope?.enabled === false) {
739
+ throw new Error("Per-request context (scope) is disabled for this instance.");
740
+ }
1252
741
 
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
- }
742
+ if (!context || typeof context !== "object") {
743
+ throw new TypeError("context must be an object.");
744
+ }
1261
745
 
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];
746
+ if (typeof fn !== "function") {
747
+ throw new TypeError("fn must be a function.");
748
+ }
1281
749
 
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
- }
750
+ const runtimeType = this.config.runtime || "async";
751
+ let requestALS;
752
+ if (runtimeType === "async") {
753
+ return import("./lib/runtime/runtime-asynclocalstorage.mjs").then((asyncRuntime) => {
754
+ requestALS = asyncRuntime.requestALS;
755
+ const parentContext = requestALS.getStore() || {};
1294
756
 
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
- );
757
+ let mergedContext;
758
+ if (this.config.scope?.merge === "deep") {
759
+ const instanceContext = this.context || {};
760
+ let temp = this._deepMerge({}, instanceContext);
761
+ temp = this._deepMerge(temp, parentContext);
762
+ mergedContext = this._deepMerge(temp, context);
763
+ } else {
764
+ mergedContext = { ...parentContext, ...context };
1309
765
  }
1310
- }
1311
-
1312
-
1313
- if (!currentTarget[finalKey]) {
1314
- currentTarget[finalKey] = {};
1315
- }
1316
- if (!currentBoundTarget[finalKey]) {
1317
- currentBoundTarget[finalKey] = {};
1318
- }
1319
766
 
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
- );
767
+ const argsArray = args || [];
768
+ return requestALS.run(mergedContext, () => fn(...argsArray));
769
+ });
1334
770
  } else {
1335
-
1336
-
1337
- currentTarget[finalKey] = newModules;
1338
- currentBoundTarget[finalKey] = newModules;
1339
- }
1340
-
1341
-
1342
- this.updateBindings(this.context, this.reference, this.boundapi);
771
+ return import("./lib/runtime/runtime-livebindings.mjs").then((liveRuntime) => {
772
+ requestALS = liveRuntime.requestALS;
773
+ const parentContext = requestALS.getStore() || {};
774
+
775
+ let mergedContext;
776
+ if (this.config.scope?.merge === "deep") {
777
+ const instanceContext = this.context || {};
778
+ let temp = this._deepMerge({}, instanceContext);
779
+ temp = this._deepMerge(temp, parentContext);
780
+ mergedContext = this._deepMerge(temp, context);
781
+ } else {
782
+ mergedContext = { ...parentContext, ...context };
783
+ }
1343
784
 
1344
- if (this.config.debug) {
1345
- console.log(`[DEBUG] addApi: Successfully added modules at ${normalizedApiPath}`);
785
+ const argsArray = args || [];
786
+ return requestALS.run(mergedContext, () => fn(...argsArray));
787
+ });
1346
788
  }
1347
789
  },
1348
790
 
1349
791
 
792
+ _deepMerge(target, source) {
793
+ return deepMerge(target, source);
794
+ },
795
+
1350
796
  async shutdown() {
1351
797
 
1352
798
 
@@ -1423,52 +869,6 @@ const slothletObject = {
1423
869
  };
1424
870
 
1425
871
 
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
872
 
1473
873
  export { slothlet };
1474
874
  export default slothlet;