@cldmv/slothlet 3.16.2 → 3.17.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.
- package/README.md +8 -7
- package/REFERENCE.md +7 -0
- package/dist/lib/builders/api_builder.mjs +1 -1
- package/dist/lib/builders/modes-processor.mjs +1 -1
- package/dist/lib/handlers/event-manager.mjs +17 -0
- package/dist/lib/handlers/lifecycle.mjs +1 -1
- package/dist/lib/handlers/module-manager.mjs +1 -1
- package/dist/lib/handlers/permission-manager.mjs +1 -1
- package/dist/lib/handlers/routine-manager.mjs +1 -1
- package/dist/lib/handlers/unified-wrapper.mjs +1 -1
- package/dist/lib/helpers/config.mjs +1 -1
- package/dist/lib/helpers/module-manifest-validator.mjs +1 -1
- package/dist/lib/helpers/pattern-matcher.mjs +1 -1
- package/dist/lib/helpers/sanitize.mjs +1 -1
- package/dist/lib/helpers/utilities.mjs +1 -1
- package/dist/lib/i18n/languages/en-us.json +8 -0
- package/dist/lib/processors/flatten.mjs +1 -1
- package/dist/lib/processors/loader.mjs +1 -1
- package/dist/slothlet.mjs +1 -1
- package/package.json +2 -1
- package/types/stub/lib/helpers/config.d.mts +11 -8
- package/types/stub/lib/helpers/pattern-matcher.d.mts +3 -1
- package/types/stub/lib/helpers/utilities.d.mts +4 -2
- package/types/stub/lib/processors/flatten.d.mts +1 -1
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import{ComponentBase}from"#factories/component-base";import{getInstanceToken}from"#handlers/lifecycle-token";class Lifecycle extends ComponentBase{static slothletProperty="lifecycle";constructor(slothlet){super(slothlet);this.subscribers=new Map;this.eventLog=[];this.maxLogSize=1e3}subscribe(event,handler){if(!this.subscribers.has(event)){this.subscribers.set(event,new Set)}this.subscribers.get(event).add(handler);return()=>{const handlers=this.subscribers.get(event);if(handlers){handlers.delete(handler)}}}on(event,handler){return this.subscribe(event,handler)}off(event,handler){const handlers=this.subscribers.get(event);if(handlers){handlers.delete(handler)}}unsubscribe(event,handler){this.off(event,handler)}async emit(event,data){if(this.____config?.debug?.lifecycle){this.eventLog.push({event,data:{...data},timestamp:Date.now()});if(this.eventLog.length>this.maxLogSize){this.eventLog.shift()}this.slothlet.debug("lifecycle",{key:"DEBUG_MODE_LIFECYCLE_EVENT",event,apiPath:data.apiPath,source:data.source,moduleID:data.moduleID})}
|
|
17
|
+
import{ComponentBase}from"#factories/component-base";import{getInstanceToken}from"#handlers/lifecycle-token";class Lifecycle extends ComponentBase{static slothletProperty="lifecycle";constructor(slothlet){super(slothlet);this.subscribers=new Map;this.internalSubscribers=new Map;this.eventLog=[];this.maxLogSize=1e3}subscribe(event,handler){if(!this.subscribers.has(event)){this.subscribers.set(event,new Set)}this.subscribers.get(event).add(handler);return()=>{const handlers=this.subscribers.get(event);if(handlers){handlers.delete(handler)}}}on(event,handler){return this.subscribe(event,handler)}subscribeInternal(event,handler){if(!this.internalSubscribers.has(event)){this.internalSubscribers.set(event,new Set)}this.internalSubscribers.get(event).add(handler);return()=>{const handlers=this.internalSubscribers.get(event);if(handlers){handlers.delete(handler)}}}off(event,handler){const handlers=this.subscribers.get(event);if(handlers){handlers.delete(handler)}}unsubscribe(event,handler){this.off(event,handler)}async emit(event,data){this.#logEvent(event,data);await this.#notify(this.subscribers.get(event),event,data)}async emitInternal(event,data){this.#logEvent(event,data);await this.#notify(this.internalSubscribers.get(event),event,data)}#logEvent(event,data){if(this.____config?.debug?.lifecycle){this.eventLog.push({event,data:{...data},timestamp:Date.now()});if(this.eventLog.length>this.maxLogSize){this.eventLog.shift()}this.slothlet.debug("lifecycle",{key:"DEBUG_MODE_LIFECYCLE_EVENT",event,apiPath:data.apiPath,source:data.source,moduleID:data.moduleID})}}async#notify(handlers,event,data){if(!handlers)return;const handlerPromises=[];const token=getInstanceToken(this.slothlet);for(const handler of handlers){try{const result=handler(data,token);if(result&&typeof result.then==="function"){handlerPromises.push(result.catch(error=>{if(!this.____config?.silent){new this.SlothletWarning("WARNING_LIFECYCLE_HANDLER_ERROR",{event},error)}}))}}catch(error){if(!this.____config?.silent){new this.SlothletWarning("WARNING_LIFECYCLE_HANDLER_ERROR",{event},error)}}}if(handlerPromises.length>0){await Promise.all(handlerPromises)}}}export{Lifecycle};
|
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import{ComponentBase}from"#factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{sortModules}from"@cldmv/slothlet/helpers/module-sort";const DEFAULT_MODULE_COLLISION_MODE="merge";class ModuleManager extends ComponentBase{static slothletProperty="moduleManager";#cache=new Map;#mounted=new Map;constructor(slothlet){super(slothlet)}async discover(options={}){await this.#emit("modules:discover-start",{scanRoot:options.scanRoot,options});const{discoverModules}=await import("@cldmv/slothlet/helpers/module-discovery");const found=await discoverModules(options);this.#cache.clear();for(const result of found){const key=`${result.packageName}@${result.manifest.version}`;this.#cache.set(key,result)}await this.#emit("modules:discover-complete",{found,stale:this.getStaleMounts()});return found}sort(results,comparator){return sortModules(results,comparator)}getDiscoveryCache(){return[...this.#cache.values()]}clearDiscoveryCache(){this.#cache.clear()}getStaleMounts(){const stale=[];for(const[key,mountResult]of this.#mounted){if(!this.#cache.has(key)){stale.push(mountResult)}}return stale}async addModule(nameOrResult,options={}){const collisionMode=options.collisionMode??DEFAULT_MODULE_COLLISION_MODE;const discoverResult=await this.#resolveToDiscoverResult(nameOrResult,options);await this.#emit("modules:mount-start",{items:[nameOrResult],options});const mountResult=await this.#mountSingle(discoverResult,collisionMode,null);await this.slothlet.handlers.routineManager?.rebuildStacks(this.slothlet.api);await this.#emit("modules:loaded",{mounted:[mountResult]});return mountResult}async addModules(items,options={}){if(!Array.isArray(items)){throw new SlothletError("INVALID_ARGUMENT",{argument:"items",expected:"array of string|DiscoverResult",received:typeof items},null,{validationError:true})}const onFailure=options.onFailure??"throw";const concurrency=Math.max(1,Number(options.concurrency)||1);const collisionMode=options.collisionMode??DEFAULT_MODULE_COLLISION_MODE;if(!["throw","rollback","best-effort"].includes(onFailure)){throw new SlothletError("INVALID_ARGUMENT",{argument:"onFailure",expected:"throw|rollback|best-effort",received:String(onFailure)},null,{validationError:true})}const resolved=[];for(const item of items){resolved.push(await this.#resolveToDiscoverResult(item,options))}const versionConfigs=this.#buildVersionConfigs(resolved);await this.#emit("modules:mount-start",{items,options});let outcome;if(concurrency===1){outcome=await this.#mountSerial(resolved,collisionMode,onFailure,versionConfigs)}else{outcome=await this.#mountParallel(resolved,collisionMode,onFailure,concurrency,versionConfigs)}await this.slothlet.handlers.routineManager?.rebuildStacks(this.slothlet.api);const loadedPayload=Array.isArray(outcome)?{mounted:outcome}:{mounted:outcome.mounted,failed:outcome.failed};await this.#emit("modules:loaded",loadedPayload);return outcome}async removeModule(name,opts={}){const matches=[];for(const[key,mountResult]of this.#mounted){if(mountResult.packageName!==name)continue;if(opts.version!==void 0&&mountResult.discoverResult.manifest.version!==opts.version)continue;matches.push({key,mountResult})}if(matches.length===0)return false;for(const{key,mountResult}of matches){await this.slothlet.handlers.apiManager.removeApiComponent(mountResult.moduleID);this.#mounted.delete(key)}return true}async#resolveToDiscoverResult(arg,options){if(arg&&typeof arg==="object"&&typeof arg.packageName==="string"){return arg}if(typeof arg!=="string"){throw new SlothletError("INVALID_ARGUMENT",{argument:"module name or DiscoverResult",expected:"string or DiscoverResult object",received:typeof arg},null,{validationError:true})}if(this.#cache.size===0){await this.discover(options.discover??{})}const candidates=[];for(const result of this.#cache.values()){if(result.packageName!==arg)continue;if(options.version!==void 0&&result.manifest.version!==options.version)continue;candidates.push(result)}if(candidates.length===0){throw new SlothletError("MODULE_PACKAGE_NOT_FOUND",{packageName:arg,hint:"addModule(name) requires the package to be installed under one of the scanned roots. Run discover() first, or pass a DiscoverResult object directly."},null,{validationError:true})}if(candidates.length>1){throw new SlothletError("INVALID_ARGUMENT",{argument:"version",expected:"version disambiguator (multi-version cache hit)",received:"undefined"},null,{validationError:true})}return candidates[0]}async#mountSingle(discoverResult,collisionMode,versionConfig){const mountPathDotted=discoverResult.mountPath.join(".");const effectiveMountPath=versionConfig?.version?`${versionConfig.version}.${mountPathDotted}`:mountPathDotted;if(collisionMode==="error"){const existing=this.#findExactMountAt(effectiveMountPath);if(existing){throw new SlothletError("MODULE_MOUNT_COLLISION",{packageName:discoverResult.packageName,mountPath:effectiveMountPath,existingModuleID:existing.moduleID,collisionMode},null,{validationError:true})}}const version=discoverResult.manifest.version;const underlyingCollisionMode=collisionMode==="error"?"merge":collisionMode;const moduleID=await this.slothlet.handlers.apiManager.addApiComponent({apiPath:mountPathDotted,folderPath:discoverResult.apiDir,options:{collisionMode:underlyingCollisionMode,metadata:{_module:{manifest:discoverResult.manifest}}},versionConfig:versionConfig??null});const result={packageName:discoverResult.packageName,mountPath:effectiveMountPath,moduleID,discoverResult,versionConfig:versionConfig??null};this.#mounted.set(`${discoverResult.packageName}@${version}`,result);await this.#emit("modules:mount-complete",{name:discoverResult.packageName,version,mountPath:effectiveMountPath,moduleID});return result}#buildVersionConfigs(resolved){const byName=new Map;for(const r of resolved){const list=byName.get(r.packageName)??[];list.push(r);byName.set(r.packageName,list)}const highestByName=new Map;for(const[name,group]of byName){if(group.length<2)continue;const versions=group.map(r=>r.manifest.version);highestByName.set(name,pickHighestSemver(versions))}const configs=new Map;for(let i=0;i<resolved.length;i++){const r=resolved[i];const group=byName.get(r.packageName);if(!group||group.length<2){configs.set(i,null);continue}const versionTag=semverToTag(r.manifest.version);const isDefault=r.manifest.version===highestByName.get(r.packageName);configs.set(i,{version:versionTag,default:isDefault})}return configs}async#emit(event,payload){const lifecycle=this.slothlet?.handlers?.lifecycle;if(lifecycle&&typeof lifecycle.emit==="function"){await lifecycle.emit(event,payload)}}#findExactMountAt(dottedPath){const history=this.slothlet?.handlers?.apiManager?.state?.addHistory;if(!Array.isArray(history))return null;for(let i=history.length-1;i>=0;i--){const entry=history[i];if(entry?.apiPath===dottedPath)return entry}return null}async#mountSerial(resolved,collisionMode,onFailure,versionConfigs){const mounted=[];const failed=[];for(let i=0;i<resolved.length;i++){const item=resolved[i];try{const mountResult=await this.#mountSingle(item,collisionMode,versionConfigs?.get(i)??null);mounted.push(mountResult)}catch(err){if(onFailure==="throw"){throw err}if(onFailure==="rollback"){await this.#rollback(mounted);throw err}failed.push({item,error:err})}}if(onFailure==="best-effort"){return{mounted,failed}}return mounted}async#mountParallel(resolved,collisionMode,onFailure,concurrency,versionConfigs){const mounted=[];const failed=[];let firstError=null;let nextIndex=0;const worker=async()=>{while(true){if(firstError&&onFailure!=="best-effort")return;const i=nextIndex++;if(i>=resolved.length)return;const item=resolved[i];try{const mountResult=await this.#mountSingle(item,collisionMode,versionConfigs?.get(i)??null);mounted.push(mountResult)}catch(err){if(onFailure==="best-effort"){failed.push({item,error:err})}else if(!firstError){firstError=err}}}};const workers=Array.from({length:Math.min(concurrency,resolved.length)},()=>worker());await Promise.all(workers);if(firstError){if(onFailure==="rollback"){await this.#rollback(mounted)}throw firstError}if(onFailure==="best-effort"){return{mounted,failed}}return mounted}async#rollback(mounted){for(const m of mounted){try{await this.slothlet.handlers.apiManager.removeApiComponent(m.moduleID);this.#mounted.delete(`${m.packageName}@${m.discoverResult.manifest.version}`)}catch{}}}}function semverToTag(version){const m=/^(\d+)/.exec(String(version));return m?`v${m[1]}`:`v${version}`}function pickHighestSemver(versions){if(versions.length===1)return versions[0];const segs=v=>String(v).split(/[.\-+]/).map(p=>{const n=Number.parseInt(p,10);return Number.isFinite(n)?n:0});let best=versions[0];let bestSegs=segs(best);for(let i=1;i<versions.length;i++){const candidate=versions[i];const candidateSegs=segs(candidate);const len=Math.max(candidateSegs.length,bestSegs.length);let candidateWins=false;for(let j=0;j<len;j++){const a=candidateSegs[j]??0;const b=bestSegs[j]??0;if(a>b){candidateWins=true;break}if(a<b)break}if(candidateWins){best=candidate;bestSegs=candidateSegs}}return best}export{ModuleManager};
|
|
17
|
+
import{ComponentBase}from"#factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{sortModules}from"@cldmv/slothlet/helpers/module-sort";const DEFAULT_MODULE_COLLISION_MODE="merge";class ModuleManager extends ComponentBase{static slothletProperty="moduleManager";#cache=new Map;#mounted=new Map;constructor(slothlet){super(slothlet)}async discover(options={}){await this.#emit("modules:discover-start",{scanRoot:options.scanRoot,options});const{discoverModules}=await import("@cldmv/slothlet/helpers/module-discovery");const found=await discoverModules(options);this.#cache.clear();for(const result of found){const key=`${result.packageName}@${result.manifest.version}`;this.#cache.set(key,result)}await this.#emit("modules:discover-complete",{found,stale:this.getStaleMounts()});return found}sort(results,comparator){return sortModules(results,comparator)}getDiscoveryCache(){return[...this.#cache.values()]}clearDiscoveryCache(){this.#cache.clear()}getStaleMounts(){const stale=[];for(const[key,mountResult]of this.#mounted){if(!this.#cache.has(key)){stale.push(mountResult)}}return stale}async addModule(nameOrResult,options={}){const collisionMode=options.collisionMode??DEFAULT_MODULE_COLLISION_MODE;const discoverResult=await this.#resolveToDiscoverResult(nameOrResult,options);await this.#emit("modules:mount-start",{items:[nameOrResult],options});const mountResult=await this.#mountSingle(discoverResult,collisionMode,null);await this.slothlet.handlers.routineManager?.rebuildStacks(this.slothlet.api);await this.#emit("modules:loaded",{mounted:[mountResult]});return mountResult}async addModules(items,options={}){if(!Array.isArray(items)){throw new SlothletError("INVALID_ARGUMENT",{argument:"items",expected:"array of string|DiscoverResult",received:typeof items},null,{validationError:true})}const onFailure=options.onFailure??"throw";const concurrency=Math.max(1,Number(options.concurrency)||1);const collisionMode=options.collisionMode??DEFAULT_MODULE_COLLISION_MODE;if(!["throw","rollback","best-effort"].includes(onFailure)){throw new SlothletError("INVALID_ARGUMENT",{argument:"onFailure",expected:"throw|rollback|best-effort",received:String(onFailure)},null,{validationError:true})}const resolved=[];for(const item of items){resolved.push(await this.#resolveToDiscoverResult(item,options))}const versionConfigs=this.#buildVersionConfigs(resolved);await this.#emit("modules:mount-start",{items,options});let outcome;if(concurrency===1){outcome=await this.#mountSerial(resolved,collisionMode,onFailure,versionConfigs)}else{outcome=await this.#mountParallel(resolved,collisionMode,onFailure,concurrency,versionConfigs)}await this.slothlet.handlers.routineManager?.rebuildStacks(this.slothlet.api);const loadedPayload=Array.isArray(outcome)?{mounted:outcome}:{mounted:outcome.mounted,failed:outcome.failed};await this.#emit("modules:loaded",loadedPayload);return outcome}async removeModule(name,opts={}){const matches=[];for(const[key,mountResult]of this.#mounted){if(mountResult.packageName!==name)continue;if(opts.version!==void 0&&mountResult.discoverResult.manifest.version!==opts.version)continue;matches.push({key,mountResult})}if(matches.length===0)return false;for(const{key,mountResult}of matches){await this.slothlet.handlers.apiManager.removeApiComponent(mountResult.moduleID);this.#mounted.delete(key)}return true}async#resolveToDiscoverResult(arg,options){if(arg&&typeof arg==="object"&&typeof arg.packageName==="string"){return arg}if(typeof arg!=="string"){throw new SlothletError("INVALID_ARGUMENT",{argument:"module name or DiscoverResult",expected:"string or DiscoverResult object",received:typeof arg},null,{validationError:true})}if(this.#cache.size===0){await this.discover(options.discover??{})}const candidates=[];for(const result of this.#cache.values()){if(result.packageName!==arg)continue;if(options.version!==void 0&&result.manifest.version!==options.version)continue;candidates.push(result)}if(candidates.length===0){throw new SlothletError("MODULE_PACKAGE_NOT_FOUND",{packageName:arg,hint:"addModule(name) requires the package to be installed under one of the scanned roots. Run discover() first, or pass a DiscoverResult object directly."},null,{validationError:true})}if(candidates.length>1){throw new SlothletError("INVALID_ARGUMENT",{argument:"version",expected:"version disambiguator (multi-version cache hit)",received:"undefined"},null,{validationError:true})}return candidates[0]}async#mountSingle(discoverResult,collisionMode,versionConfig){const mountPathDotted=discoverResult.mountPath.join(".");const effectiveMountPath=versionConfig?.version?`${versionConfig.version}.${mountPathDotted}`:mountPathDotted;if(collisionMode==="error"){const existing=this.#findExactMountAt(effectiveMountPath);if(existing){throw new SlothletError("MODULE_MOUNT_COLLISION",{packageName:discoverResult.packageName,mountPath:effectiveMountPath,existingModuleID:existing.moduleID,collisionMode},null,{validationError:true})}}const version=discoverResult.manifest.version;const underlyingCollisionMode=collisionMode==="error"?"merge":collisionMode;const moduleID=await this.slothlet.handlers.apiManager.addApiComponent({apiPath:mountPathDotted,folderPath:discoverResult.apiDir,options:{collisionMode:underlyingCollisionMode,metadata:{_module:{manifest:discoverResult.manifest}}},versionConfig:versionConfig??null});const permissionManager=this.slothlet.handlers?.permissionManager;if(permissionManager){const manifest=discoverResult.manifest;if(Array.isArray(manifest.permissions)){for(const rule of manifest.permissions){permissionManager.addRule(rule,moduleID,null,"manifest")}}if(Array.isArray(manifest.events)){for(const rule of manifest.events){permissionManager.addEventRule(rule,moduleID,null,"manifest")}}}const result={packageName:discoverResult.packageName,mountPath:effectiveMountPath,moduleID,discoverResult,versionConfig:versionConfig??null};this.#mounted.set(`${discoverResult.packageName}@${version}`,result);await this.#emit("modules:mount-complete",{name:discoverResult.packageName,version,mountPath:effectiveMountPath,moduleID});return result}#buildVersionConfigs(resolved){const byName=new Map;for(const r of resolved){const list=byName.get(r.packageName)??[];list.push(r);byName.set(r.packageName,list)}const highestByName=new Map;for(const[name,group]of byName){if(group.length<2)continue;const versions=group.map(r=>r.manifest.version);highestByName.set(name,pickHighestSemver(versions))}const configs=new Map;for(let i=0;i<resolved.length;i++){const r=resolved[i];const group=byName.get(r.packageName);if(!group||group.length<2){configs.set(i,null);continue}const versionTag=semverToTag(r.manifest.version);const isDefault=r.manifest.version===highestByName.get(r.packageName);configs.set(i,{version:versionTag,default:isDefault})}return configs}async#emit(event,payload){const lifecycle=this.slothlet?.handlers?.lifecycle;if(lifecycle&&typeof lifecycle.emit==="function"){await lifecycle.emit(event,payload)}}#findExactMountAt(dottedPath){const history=this.slothlet?.handlers?.apiManager?.state?.addHistory;if(!Array.isArray(history))return null;for(let i=history.length-1;i>=0;i--){const entry=history[i];if(entry?.apiPath===dottedPath)return entry}return null}async#mountSerial(resolved,collisionMode,onFailure,versionConfigs){const mounted=[];const failed=[];for(let i=0;i<resolved.length;i++){const item=resolved[i];try{const mountResult=await this.#mountSingle(item,collisionMode,versionConfigs?.get(i)??null);mounted.push(mountResult)}catch(err){if(onFailure==="throw"){throw err}if(onFailure==="rollback"){await this.#rollback(mounted);throw err}failed.push({item,error:err})}}if(onFailure==="best-effort"){return{mounted,failed}}return mounted}async#mountParallel(resolved,collisionMode,onFailure,concurrency,versionConfigs){const mounted=[];const failed=[];let firstError=null;let nextIndex=0;const worker=async()=>{while(true){if(firstError&&onFailure!=="best-effort")return;const i=nextIndex++;if(i>=resolved.length)return;const item=resolved[i];try{const mountResult=await this.#mountSingle(item,collisionMode,versionConfigs?.get(i)??null);mounted.push(mountResult)}catch(err){if(onFailure==="best-effort"){failed.push({item,error:err})}else if(!firstError){firstError=err}}}};const workers=Array.from({length:Math.min(concurrency,resolved.length)},()=>worker());await Promise.all(workers);if(firstError){if(onFailure==="rollback"){await this.#rollback(mounted)}throw firstError}if(onFailure==="best-effort"){return{mounted,failed}}return mounted}async#rollback(mounted){for(const m of mounted){try{await this.slothlet.handlers.apiManager.removeApiComponent(m.moduleID);this.#mounted.delete(`${m.packageName}@${m.discoverResult.manifest.version}`)}catch{}}}}function semverToTag(version){const m=/^(\d+)/.exec(String(version));return m?`v${m[1]}`:`v${version}`}function pickHighestSemver(versions){if(versions.length===1)return versions[0];const segs=v=>String(v).split(/[.\-+]/).map(p=>{const n=Number.parseInt(p,10);return Number.isFinite(n)?n:0});let best=versions[0];let bestSegs=segs(best);for(let i=1;i<versions.length;i++){const candidate=versions[i];const candidateSegs=segs(candidate);const len=Math.max(candidateSegs.length,bestSegs.length);let candidateWins=false;for(let j=0;j<len;j++){const a=candidateSegs[j]??0;const b=bestSegs[j]??0;if(a>b){candidateWins=true;break}if(a<b)break}if(candidateWins){best=candidate;bestSegs=candidateSegs}}return best}export{ModuleManager};
|
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import{path}from"@cldmv/slothlet/helpers/platform";import{ComponentBase}from"#factories/component-base";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";import{translate}from"@cldmv/slothlet/i18n";let ruleIdCounter=0;const HOOK_TARGET_TYPES=new Set(["before","after","always","error","hook"]);function runtime_isPrivateName(targetPath){if(typeof targetPath!=="string"||targetPath.length===0)return false;const cut=targetPath.lastIndexOf(".");if(cut<0)return false;return targetPath.charCodeAt(cut+1)===95}function runtime_sameModuleDir(callerFilePath,targetFilePath){return path.dirname(callerFilePath)===path.dirname(targetFilePath)}class PermissionManager extends ComponentBase{static slothletProperty="permissionManager";#rules=new Map;#defaultPolicy="allow";#enabled=false;#audit="default";#readGating=false;#capture=true;#privateHost="deny";#sealed=false;#resolvedCache=new Map;#compiledCache=new Map;constructor(slothlet){super(slothlet);const permConfig=slothlet.config?.permissions;if(permConfig){this.#defaultPolicy=permConfig.defaultPolicy||"allow";this.#enabled=permConfig.enabled!==false;this.#audit=permConfig.audit||"default";this.#readGating=permConfig.readGating!==false;this.#capture=permConfig.references?.capture!==false;this.#privateHost=permConfig.private?.host==="allow"?"allow":"deny";if(Array.isArray(permConfig.rules)){for(const rule of permConfig.rules){this.addRule(rule,null)}}}this.addRule({caller:"**",target:"slothlet.permissions.control.**",effect:"deny"},"__builtin__");this.addRule({caller:"**",target:"slothlet.lockCaller",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.bind",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.hook.**",effect:"deny"},"__builtin__");this.addRule({caller:"**",target:"slothlet.hook.list",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.hook.on",effect:"allow"},"__builtin__")}addRule(rule,ownerModuleID=null,ruleId=null){this.#assertNotSealed();this.#validateRule(rule);const id=ruleId||`perm-${++ruleIdCounter}`;const hookTarget=this.#parseHookTarget(rule.target);const entry={id,caller:rule.caller,target:rule.target,effect:rule.effect,condition:rule.condition??null,hookType:hookTarget?hookTarget.hookType:null,hookPathPattern:hookTarget?hookTarget.pathPattern:null,ownerModuleID,registeredAt:Date.now()};this.#rules.set(id,entry);this.#clearCache();this.debug("permissions",{key:"DEBUG_PERMISSION_RULE_ADDED",ruleId:id,caller:rule.caller,target:rule.target,effect:rule.effect,ownerModuleID});return id}removeRule(ruleId,callerModuleID=null){this.#assertNotSealed();const entry=this.#rules.get(ruleId);if(!entry)return false;if(callerModuleID&&entry.ownerModuleID&&callerModuleID===entry.ownerModuleID){throw new this.SlothletError("PERMISSION_SELF_MODIFY",{ruleId,moduleID:callerModuleID})}this.#rules.delete(ruleId);this.#clearCache();this.debug("permissions",{key:"DEBUG_PERMISSION_RULE_REMOVED",ruleId,caller:entry.caller,target:entry.target,effect:entry.effect});return true}checkAccess(callerPath,targetPath,callerFilePath=null,targetFilePath=null,runtimeContext=null,options=null){const normalizedOptions=options==null?{}:options;if(typeof normalizedOptions!=="object"||Array.isArray(normalizedOptions)){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"options",expected:"object with optional boolean useCache",received:Array.isArray(normalizedOptions)?"array":typeof normalizedOptions,validationError:true})}const{useCache=true}=normalizedOptions;if(typeof useCache!=="boolean"){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"options.useCache",expected:"boolean",received:typeof useCache,validationError:true})}return this.#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,useCache).allowed}matchesCondition(condition,runtimeContext=null){if(condition==null)return true;this.#assertValidConditionPayload(condition,"INVALID_ARGUMENT");return this.#matchesConditionUnchecked(condition,runtimeContext)}#matchesConditionUnchecked(condition,runtimeContext=null){if(condition==null)return true;const ctx=runtimeContext??{};if(Array.isArray(condition)){return condition.some(entry=>this.#singleConditionMatches(entry,ctx))}return this.#singleConditionMatches(condition,ctx)}enforceAccess(callerPath,targetPath,callerFilePath=null,targetFilePath=null,runtimeContext=null){const result=this.#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,true);if(result.event){this.#emitAuditEvent(result.event,result.payload)}return result.allowed}enforceHookAccess(callerPath,hookPath,hookType,callerFilePath=null,targetFilePath=null,runtimeContext=null){const result=this.#resolveHookAccess(callerPath,hookPath,hookType,callerFilePath,targetFilePath,runtimeContext);if(result.event){this.#emitAuditEvent(result.event,result.payload)}return result.allowed}checkHookAccess(callerPath,hookPath,hookType,callerFilePath=null,targetFilePath=null,runtimeContext=null){return this.#resolveHookAccess(callerPath,hookPath,hookType,callerFilePath,targetFilePath,runtimeContext).allowed}getRulesForPath(targetPath){const matching=[];for(const entry of this.#rules.values()){const targetMatcher=this.#getCompiledPattern(entry.target);if(targetMatcher(targetPath)){matching.push(this.#serializeRule(entry))}}return matching}getRulesByModule(moduleID){const matching=[];for(const entry of this.#rules.values()){if(entry.ownerModuleID===moduleID){matching.push(this.#serializeRule(entry))}}return matching}getRulesForCaller(callerPath){const matching=[];for(const entry of this.#rules.values()){const callerMatcher=this.#getCompiledPattern(entry.caller);if(callerMatcher(callerPath)){matching.push(this.#serializeRule(entry))}}return matching}enable(){this.#assertNotSealed();this.#enabled=true;this.#clearCache()}#assertNotSealed(){if(this.#sealed){throw new this.SlothletError("PERMISSION_SEALED",{},null,{validationError:true})}}seal(){this.#sealed=true}isSealed(){return this.#sealed}disable(){this.#assertNotSealed();this.#enabled=false;this.#clearCache()}isEnabled(){return this.#enabled}isReadGatingEnabled(){return this.#readGating}isCaptureEnabled(){return this.#capture}isPrivateTarget(targetPath){return runtime_isPrivateName(targetPath)}setReadGating(value){this.#assertNotSealed();if(typeof value!=="boolean"){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"value",expected:"boolean",received:typeof value,validationError:true})}this.#readGating=value}exportRules(){const rules=[];for(const entry of this.#rules.values()){rules.push({rule:{caller:entry.caller,target:entry.target,effect:entry.effect},ownerModuleID:entry.ownerModuleID})}return rules}importRules(registrations){if(!Array.isArray(registrations))return;for(const reg of registrations){this.addRule(reg.rule,reg.ownerModuleID)}}async shutdown(){this.#rules.clear();this.#resolvedCache.clear();this.#compiledCache.clear();this.#enabled=false;this.#defaultPolicy="allow";this.#audit="default";this.#readGating=false}#validateRule(rule){if(!rule||typeof rule!=="object"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_NOT_OBJECT"),received:typeof rule})}if(typeof rule.caller!=="string"||!rule.caller){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_CALLER_REQUIRED"),received:typeof rule.caller})}if(typeof rule.target!=="string"||!rule.target){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_TARGET_REQUIRED"),received:typeof rule.target})}if(rule.effect!=="allow"&&rule.effect!=="deny"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_EFFECT_INVALID"),received:rule.effect})}if(rule.condition!==void 0&&rule.condition!==null){this.#assertValidConditionPayload(rule.condition,"INVALID_PERMISSION_RULE")}}#assertValidConditionPayload(condition,errorCode){const getValueType=value=>{if(value===null)return"null";if(Array.isArray(value))return"array";return typeof value};const isPlainObject=value=>{if(value===null||typeof value!=="object")return false;const proto=Object.getPrototypeOf(value);return proto===Object.prototype||proto===null};const isValidConditionEntry=value=>typeof value==="function"||isPlainObject(value);const entries=Array.isArray(condition)?condition:[condition];const invalidEntry=entries.length===0?condition:entries.find(entry=>!isValidConditionEntry(entry));if(entries.length>0&&invalidEntry===void 0)return;const received=getValueType(invalidEntry);if(errorCode==="INVALID_PERMISSION_RULE"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_CONDITION_INVALID"),received})}throw new this.SlothletError("INVALID_ARGUMENT",{argument:"condition",expected:translate("PERM_RULE_CONDITION_INVALID"),received,validationError:true})}#deepObjectMatches(pattern,ctx){if(ctx==null||typeof ctx!=="object")return false;for(const[key,val]of Object.entries(pattern)){const proto=val!==null&&typeof val==="object"?Object.getPrototypeOf(val):null;const isPlainNested=proto===Object.prototype||proto===null;if(val!==null&&typeof val==="object"&&isPlainNested){if(!this.#deepObjectMatches(val,ctx[key]))return false}else{if(ctx[key]!==val)return false}}return true}#singleConditionMatches(conditionEntry,ctx){if(typeof conditionEntry==="function"){try{return!!conditionEntry(ctx)}catch{return false}}return this.#deepObjectMatches(conditionEntry,ctx)}#conditionMatches(entry,runtimeContext){return this.#matchesConditionUnchecked(entry.condition,runtimeContext)}#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,useCache){const isControlTarget=targetPath?.startsWith("slothlet.permissions.control.");if(!this.#enabled&&!isControlTarget)return{allowed:true,event:null,payload:null};if(callerFilePath&&targetFilePath&&callerFilePath===targetFilePath){return{allowed:true,event:"permission:self-bypass",payload:{caller:callerPath,target:targetPath,filePath:callerFilePath}}}if(runtime_isPrivateName(targetPath)&&targetPath!=="slothlet"&&!targetPath.startsWith("slothlet.")){if(callerFilePath&&targetFilePath&&runtime_sameModuleDir(callerFilePath,targetFilePath)){return{allowed:true,event:"permission:private-module-bypass",payload:{caller:callerPath,target:targetPath,filePath:callerFilePath}}}if(!callerFilePath&&this.#privateHost==="allow"){return{allowed:true,event:"permission:private-host-allow",payload:{caller:callerPath??null,target:targetPath}}}return{allowed:false,event:"permission:denied",payload:{caller:callerPath??null,target:targetPath}}}const cacheKey=`${callerPath}::${targetPath}`;if(useCache&&this.#resolvedCache.has(cacheKey)){return this.#resolvedCache.get(cacheKey)}const entry=this.#evaluate(callerPath,targetPath,runtimeContext);if(useCache&&!entry.hasConditionalRules){this.#resolvedCache.set(cacheKey,entry)}return entry}#evaluate(callerPath,targetPath,runtimeContext=null){const matches=[];for(const entry of this.#rules.values()){if(entry.hookType!=null)continue;const callerMatcher=this.#getCompiledPattern(entry.caller);const targetMatcher=this.#getCompiledPattern(entry.target);if(callerMatcher(callerPath)&&targetMatcher(targetPath)){matches.push(entry)}}const hasConditionalRules=matches.some(m=>m.condition!=null);const conditioned=matches.filter(entry=>this.#conditionMatches(entry,runtimeContext));if(conditioned.length===0){const allowed2=this.#defaultPolicy==="allow";return{allowed:allowed2,event:"permission:default",payload:{caller:callerPath,target:targetPath,policy:this.#defaultPolicy},hasConditionalRules}}conditioned.sort((a,b)=>{const specA=this.#computeSpecificity(a,callerPath,targetPath);const specB=this.#computeSpecificity(b,callerPath,targetPath);if(specA!==specB)return specB-specA;return a.registeredAt-b.registeredAt});const highestSpec=this.#computeSpecificity(conditioned[0],callerPath,targetPath);const topTier=conditioned.filter(m=>this.#computeSpecificity(m,callerPath,targetPath)===highestSpec);const winner=topTier[topTier.length-1];const allowed=winner.effect==="allow";return{allowed,event:allowed?"permission:allowed":"permission:denied",payload:{caller:callerPath,target:targetPath,rule:this.#serializeRule(winner),conditionMatched:winner.condition!=null},hasConditionalRules}}#resolveHookAccess(callerPath,hookPath,hookType,callerFilePath,targetFilePath,runtimeContext){if(callerPath==null)return{allowed:true,event:null,payload:null};const hookDecision=this.#evaluateHook(callerPath,hookPath,hookType,runtimeContext);if(hookDecision.matched){return{allowed:hookDecision.allowed,event:hookDecision.event,payload:hookDecision.payload}}const callDecision=this.#resolveAccess(callerPath,hookPath,callerFilePath,targetFilePath,runtimeContext,true);return{allowed:callDecision.allowed,event:callDecision.event,payload:callDecision.payload}}#evaluateHook(callerPath,hookPath,hookType,runtimeContext){const matches=[];for(const entry of this.#rules.values()){if(entry.hookType==null)continue;if(entry.hookType!=="hook"&&entry.hookType!==hookType)continue;const callerMatcher=this.#getCompiledPattern(entry.caller);const pathMatcher=this.#getCompiledPattern(entry.hookPathPattern);if(callerMatcher(callerPath)&&pathMatcher(hookPath)){matches.push(entry)}}const conditioned=matches.filter(entry=>this.#conditionMatches(entry,runtimeContext));if(conditioned.length===0){return{matched:false,allowed:false,event:null,payload:null}}const spec=e=>this.#patternSpecificity(e.caller,callerPath)+this.#patternSpecificity(e.hookPathPattern,hookPath)+(e.hookType==="hook"?0:1);conditioned.sort((a,b)=>{const specA=spec(a);const specB=spec(b);if(specA!==specB)return specB-specA;return a.registeredAt-b.registeredAt});const highest=spec(conditioned[0]);const topTier=conditioned.filter(m=>spec(m)===highest);const winner=topTier[topTier.length-1];const allowed=winner.effect==="allow";return{matched:true,allowed,event:allowed?"permission:allowed":"permission:denied",payload:{caller:callerPath,target:`${hookPath}:${hookType}`,rule:this.#serializeRule(winner),conditionMatched:winner.condition!=null}}}#parseHookTarget(target){const lastColon=target.lastIndexOf(":");if(lastColon===-1)return null;const type=target.substring(lastColon+1);if(!HOOK_TARGET_TYPES.has(type))return null;const pathPattern=target.substring(0,lastColon);if(!pathPattern)return null;return{pathPattern,hookType:type}}#computeSpecificity(entry,callerPath,targetPath){return this.#patternSpecificity(entry.caller,callerPath)+this.#patternSpecificity(entry.target,targetPath)}#patternSpecificity(pattern,___path){if(!pattern.includes("*")&&!pattern.includes("?")&&!pattern.includes("{")){return 3}if(pattern.includes("**")){return 1}return 2}#getCompiledPattern(pattern){let matcher=this.#compiledCache.get(pattern);if(!matcher){matcher=compilePattern(pattern);this.#compiledCache.set(pattern,matcher)}return matcher}#clearCache(){this.#resolvedCache.clear()}#emitAuditEvent(event,payload){this.debug("permissions",{key:event==="permission:denied"?"DEBUG_PERMISSION_DENIED":event==="permission:allowed"?"DEBUG_PERMISSION_ALLOWED":event==="permission:self-bypass"?"DEBUG_PERMISSION_SELF_BYPASS":"DEBUG_PERMISSION_DEFAULT",...payload});const alwaysEmit=event==="permission:denied"||event==="permission:self-bypass";if(!alwaysEmit&&this.#audit!=="verbose")return;const lifecycle=this.slothlet.handlers?.lifecycle;if(lifecycle){lifecycle.emit(event,{...payload,timestamp:Date.now()})}}#serializeRule(entry){return{id:entry.id,caller:entry.caller,target:entry.target,effect:entry.effect,condition:entry.condition??null,ownerModuleID:entry.ownerModuleID,registeredAt:entry.registeredAt}}debug(category,data){this.slothlet.debug(category,data)}}export{PermissionManager};
|
|
17
|
+
import{path}from"@cldmv/slothlet/helpers/platform";import{ComponentBase}from"#factories/component-base";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";import{translate}from"@cldmv/slothlet/i18n";let ruleIdCounter=0;let ruleRegistrationSeq=0;const HOOK_TARGET_TYPES=new Set(["before","after","always","error","hook"]);const RULE_LAYER_RANK=Object.freeze({builtin:0,manifest:1,instance:2,runtime:3});function resolveRuleLayer(layer,ownerModuleID){if(layer&&Object.prototype.hasOwnProperty.call(RULE_LAYER_RANK,layer))return layer;if(ownerModuleID==="__builtin__")return"builtin";return"instance"}function runtime_isPrivateName(targetPath){if(typeof targetPath!=="string"||targetPath.length===0)return false;const cut=targetPath.lastIndexOf(".");if(cut<0)return false;return targetPath.charCodeAt(cut+1)===95}function runtime_sameModuleDir(callerFilePath,targetFilePath){return path.dirname(callerFilePath)===path.dirname(targetFilePath)}class PermissionManager extends ComponentBase{static slothletProperty="permissionManager";#rules=new Map;#defaultPolicy="allow";#enabled=false;#audit="default";#readGating=false;#capture=true;#privateHost="deny";#eventRules=new Map;#eventDefault="notify";#eventRulesEpoch=0;#sealed=false;#resolvedCache=new Map;#compiledCache=new Map;constructor(slothlet){super(slothlet);const permConfig=slothlet.config?.permissions;if(permConfig){this.#defaultPolicy=permConfig.defaultPolicy||"allow";this.#enabled=permConfig.enabled!==false;this.#audit=permConfig.audit||"default";this.#readGating=permConfig.readGating!==false;this.#capture=permConfig.references?.capture!==false;this.#privateHost=permConfig.private?.host==="allow"?"allow":"deny";if(Array.isArray(permConfig.rules)){for(const rule of permConfig.rules){this.addRule(rule,null)}}if(permConfig.events){if(permConfig.events.default)this.#eventDefault=permConfig.events.default;if(Array.isArray(permConfig.events.rules)){for(const rule of permConfig.events.rules){this.addEventRule(rule,null)}}}}this.addRule({caller:"**",target:"slothlet.permissions.control.**",effect:"deny"},"__builtin__");this.addRule({caller:"**",target:"slothlet.lockCaller",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.bind",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.hook.**",effect:"deny"},"__builtin__");this.addRule({caller:"**",target:"slothlet.hook.list",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.hook.on",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.event.**",effect:"deny"},"__builtin__");this.addRule({caller:"**",target:"slothlet.event.on",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.event.once",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.event.off",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.event.emit",effect:"allow"},"__builtin__")}addRule(rule,ownerModuleID=null,ruleId=null,layer=null){this.#assertNotSealed();this.#validateRule(rule);const id=ruleId||`perm-${++ruleIdCounter}`;const hookTarget=this.#parseHookTarget(rule.target);const ruleLayer=resolveRuleLayer(layer,ownerModuleID);const entry={id,caller:rule.caller,target:rule.target,effect:rule.effect,condition:rule.condition??null,hookType:hookTarget?hookTarget.hookType:null,hookPathPattern:hookTarget?hookTarget.pathPattern:null,ownerModuleID,layer:ruleLayer,layerRank:RULE_LAYER_RANK[ruleLayer],registeredAt:Date.now(),registrationSeq:++ruleRegistrationSeq};this.#rules.set(id,entry);this.#clearCache();this.debug("permissions",{key:"DEBUG_PERMISSION_RULE_ADDED",ruleId:id,caller:rule.caller,target:rule.target,effect:rule.effect,ownerModuleID});return id}removeRule(ruleId,callerModuleID=null){this.#assertNotSealed();const entry=this.#rules.get(ruleId);if(!entry)return false;if(callerModuleID&&entry.ownerModuleID&&callerModuleID===entry.ownerModuleID){throw new this.SlothletError("PERMISSION_SELF_MODIFY",{ruleId,moduleID:callerModuleID})}this.#rules.delete(ruleId);this.#clearCache();this.debug("permissions",{key:"DEBUG_PERMISSION_RULE_REMOVED",ruleId,caller:entry.caller,target:entry.target,effect:entry.effect});return true}addEventRule(rule,ownerModuleID=null,ruleId=null,layer=null){this.#assertNotSealed();this.#validateEventRule(rule);const id=ruleId||`evt-${++ruleIdCounter}`;const ruleLayer=resolveRuleLayer(layer,ownerModuleID);const entry={id,caller:rule.caller,event:rule.event,effect:rule.effect,condition:rule.condition??null,ownerModuleID,layer:ruleLayer,layerRank:RULE_LAYER_RANK[ruleLayer],registeredAt:Date.now(),registrationSeq:++ruleRegistrationSeq};this.#eventRules.set(id,entry);this.#eventRulesEpoch++;this.debug("permissions",{key:"DEBUG_PERMISSION_RULE_ADDED",ruleId:id,caller:rule.caller,target:`${rule.event}:event`,effect:rule.effect,ownerModuleID});return id}removeEventRule(ruleId,callerModuleID=null){this.#assertNotSealed();const entry=this.#eventRules.get(ruleId);if(!entry)return false;if(callerModuleID&&entry.ownerModuleID&&callerModuleID===entry.ownerModuleID){throw new this.SlothletError("PERMISSION_SELF_MODIFY",{ruleId,moduleID:callerModuleID})}this.#eventRules.delete(ruleId);this.#eventRulesEpoch++;return true}resolveEventLevel(subscriberPath,eventName,runtimeContext=null){if(subscriberPath==null)return"allow";const matches=[];for(const entry of this.#eventRules.values()){const callerMatcher=this.#getCompiledPattern(entry.caller);const eventMatcher=this.#getCompiledPattern(entry.event);if(callerMatcher(subscriberPath)&&eventMatcher(eventName))matches.push(entry)}const conditioned=matches.filter(entry=>this.#conditionMatches(entry,runtimeContext));if(conditioned.length===0)return this.#eventDefault;conditioned.sort((a,b)=>{const specA=this.#patternSpecificity(a.caller,subscriberPath)+this.#patternSpecificity(a.event,eventName);const specB=this.#patternSpecificity(b.caller,subscriberPath)+this.#patternSpecificity(b.event,eventName);if(specA!==specB)return specB-specA;if(a.layerRank!==b.layerRank)return b.layerRank-a.layerRank;return b.registrationSeq-a.registrationSeq});return conditioned[0].effect}get eventRulesEpoch(){return this.#eventRulesEpoch}get hasConditionalEventRules(){for(const entry of this.#eventRules.values()){if(entry.condition!=null)return true}return false}#validateEventRule(rule){if(!rule||typeof rule!=="object"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_NOT_OBJECT"),received:typeof rule})}if(typeof rule.caller!=="string"||!rule.caller){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_CALLER_REQUIRED"),received:typeof rule.caller})}if(typeof rule.event!=="string"||!rule.event){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_EVENT_RULE_EVENT_REQUIRED"),received:typeof rule.event})}if(rule.effect!=="deny"&&rule.effect!=="notify"&&rule.effect!=="allow"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_EVENT_RULE_EFFECT_INVALID"),received:rule.effect})}if(rule.condition!==void 0&&rule.condition!==null){this.#assertValidConditionPayload(rule.condition,"INVALID_PERMISSION_RULE")}}checkAccess(callerPath,targetPath,callerFilePath=null,targetFilePath=null,runtimeContext=null,options=null){const normalizedOptions=options==null?{}:options;if(typeof normalizedOptions!=="object"||Array.isArray(normalizedOptions)){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"options",expected:"object with optional boolean useCache",received:Array.isArray(normalizedOptions)?"array":typeof normalizedOptions,validationError:true})}const{useCache=true}=normalizedOptions;if(typeof useCache!=="boolean"){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"options.useCache",expected:"boolean",received:typeof useCache,validationError:true})}return this.#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,useCache).allowed}matchesCondition(condition,runtimeContext=null){if(condition==null)return true;this.#assertValidConditionPayload(condition,"INVALID_ARGUMENT");return this.#matchesConditionUnchecked(condition,runtimeContext)}#matchesConditionUnchecked(condition,runtimeContext=null){if(condition==null)return true;const ctx=runtimeContext??{};if(Array.isArray(condition)){return condition.some(entry=>this.#singleConditionMatches(entry,ctx))}return this.#singleConditionMatches(condition,ctx)}enforceAccess(callerPath,targetPath,callerFilePath=null,targetFilePath=null,runtimeContext=null){const result=this.#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,true);if(result.event){this.#emitAuditEvent(result.event,result.payload)}return result.allowed}enforceHookAccess(callerPath,hookPath,hookType,callerFilePath=null,targetFilePath=null,runtimeContext=null){const result=this.#resolveHookAccess(callerPath,hookPath,hookType,callerFilePath,targetFilePath,runtimeContext);if(result.event){this.#emitAuditEvent(result.event,result.payload)}return result.allowed}checkHookAccess(callerPath,hookPath,hookType,callerFilePath=null,targetFilePath=null,runtimeContext=null){return this.#resolveHookAccess(callerPath,hookPath,hookType,callerFilePath,targetFilePath,runtimeContext).allowed}getRulesForPath(targetPath){const matching=[];for(const entry of this.#rules.values()){const targetMatcher=this.#getCompiledPattern(entry.target);if(targetMatcher(targetPath)){matching.push(this.#serializeRule(entry))}}return matching}getRulesByModule(moduleID){const matching=[];for(const entry of this.#rules.values()){if(entry.ownerModuleID===moduleID){matching.push(this.#serializeRule(entry))}}return matching}getRulesForCaller(callerPath){const matching=[];for(const entry of this.#rules.values()){const callerMatcher=this.#getCompiledPattern(entry.caller);if(callerMatcher(callerPath)){matching.push(this.#serializeRule(entry))}}return matching}enable(){this.#assertNotSealed();this.#enabled=true;this.#clearCache()}#assertNotSealed(){if(this.#sealed){throw new this.SlothletError("PERMISSION_SEALED",{},null,{validationError:true})}}seal(){this.#sealed=true}isSealed(){return this.#sealed}disable(){this.#assertNotSealed();this.#enabled=false;this.#clearCache()}isEnabled(){return this.#enabled}isReadGatingEnabled(){return this.#readGating}isCaptureEnabled(){return this.#capture}isPrivateTarget(targetPath){return runtime_isPrivateName(targetPath)}setReadGating(value){this.#assertNotSealed();if(typeof value!=="boolean"){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"value",expected:"boolean",received:typeof value,validationError:true})}this.#readGating=value}exportRules(){const rules=[];for(const entry of this.#rules.values()){rules.push({rule:{caller:entry.caller,target:entry.target,effect:entry.effect},ownerModuleID:entry.ownerModuleID})}return rules}importRules(registrations){if(!Array.isArray(registrations))return;for(const reg of registrations){this.addRule(reg.rule,reg.ownerModuleID)}}async shutdown(){this.#rules.clear();this.#resolvedCache.clear();this.#compiledCache.clear();this.#enabled=false;this.#defaultPolicy="allow";this.#audit="default";this.#readGating=false}#validateRule(rule){if(!rule||typeof rule!=="object"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_NOT_OBJECT"),received:typeof rule})}if(typeof rule.caller!=="string"||!rule.caller){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_CALLER_REQUIRED"),received:typeof rule.caller})}if(typeof rule.target!=="string"||!rule.target){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_TARGET_REQUIRED"),received:typeof rule.target})}if(rule.effect!=="allow"&&rule.effect!=="deny"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_EFFECT_INVALID"),received:rule.effect})}if(rule.condition!==void 0&&rule.condition!==null){this.#assertValidConditionPayload(rule.condition,"INVALID_PERMISSION_RULE")}}#assertValidConditionPayload(condition,errorCode){const getValueType=value=>{if(value===null)return"null";if(Array.isArray(value))return"array";return typeof value};const isPlainObject=value=>{if(value===null||typeof value!=="object")return false;const proto=Object.getPrototypeOf(value);return proto===Object.prototype||proto===null};const isValidConditionEntry=value=>typeof value==="function"||isPlainObject(value);const entries=Array.isArray(condition)?condition:[condition];const invalidEntry=entries.length===0?condition:entries.find(entry=>!isValidConditionEntry(entry));if(entries.length>0&&invalidEntry===void 0)return;const received=getValueType(invalidEntry);if(errorCode==="INVALID_PERMISSION_RULE"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_CONDITION_INVALID"),received})}throw new this.SlothletError("INVALID_ARGUMENT",{argument:"condition",expected:translate("PERM_RULE_CONDITION_INVALID"),received,validationError:true})}#deepObjectMatches(pattern,ctx){if(ctx==null||typeof ctx!=="object")return false;for(const[key,val]of Object.entries(pattern)){const proto=val!==null&&typeof val==="object"?Object.getPrototypeOf(val):null;const isPlainNested=proto===Object.prototype||proto===null;if(val!==null&&typeof val==="object"&&isPlainNested){if(!this.#deepObjectMatches(val,ctx[key]))return false}else{if(ctx[key]!==val)return false}}return true}#singleConditionMatches(conditionEntry,ctx){if(typeof conditionEntry==="function"){try{return!!conditionEntry(ctx)}catch{return false}}return this.#deepObjectMatches(conditionEntry,ctx)}#conditionMatches(entry,runtimeContext){return this.#matchesConditionUnchecked(entry.condition,runtimeContext)}#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,useCache){const isControlTarget=targetPath?.startsWith("slothlet.permissions.control.");if(!this.#enabled&&!isControlTarget)return{allowed:true,event:null,payload:null};if(callerFilePath&&targetFilePath&&callerFilePath===targetFilePath){return{allowed:true,event:"permission:self-bypass",payload:{caller:callerPath,target:targetPath,filePath:callerFilePath}}}if(runtime_isPrivateName(targetPath)&&targetPath!=="slothlet"&&!targetPath.startsWith("slothlet.")){if(callerFilePath&&targetFilePath&&runtime_sameModuleDir(callerFilePath,targetFilePath)){return{allowed:true,event:"permission:private-module-bypass",payload:{caller:callerPath,target:targetPath,filePath:callerFilePath}}}if(!callerFilePath&&this.#privateHost==="allow"){return{allowed:true,event:"permission:private-host-allow",payload:{caller:callerPath??null,target:targetPath}}}return{allowed:false,event:"permission:denied",payload:{caller:callerPath??null,target:targetPath}}}const cacheKey=`${callerPath}::${targetPath}`;if(useCache&&this.#resolvedCache.has(cacheKey)){return this.#resolvedCache.get(cacheKey)}const entry=this.#evaluate(callerPath,targetPath,runtimeContext);if(useCache&&!entry.hasConditionalRules){this.#resolvedCache.set(cacheKey,entry)}return entry}#evaluate(callerPath,targetPath,runtimeContext=null){const matches=[];for(const entry of this.#rules.values()){if(entry.hookType!=null)continue;const callerMatcher=this.#getCompiledPattern(entry.caller);const targetMatcher=this.#getCompiledPattern(entry.target);if(callerMatcher(callerPath)&&targetMatcher(targetPath)){matches.push(entry)}}const hasConditionalRules=matches.some(m=>m.condition!=null);const conditioned=matches.filter(entry=>this.#conditionMatches(entry,runtimeContext));if(conditioned.length===0){const allowed2=this.#defaultPolicy==="allow";return{allowed:allowed2,event:"permission:default",payload:{caller:callerPath,target:targetPath,policy:this.#defaultPolicy},hasConditionalRules}}conditioned.sort((a,b)=>{const specA=this.#computeSpecificity(a,callerPath,targetPath);const specB=this.#computeSpecificity(b,callerPath,targetPath);if(specA!==specB)return specB-specA;if(a.layerRank!==b.layerRank)return b.layerRank-a.layerRank;return b.registrationSeq-a.registrationSeq});const winner=conditioned[0];const allowed=winner.effect==="allow";return{allowed,event:allowed?"permission:allowed":"permission:denied",payload:{caller:callerPath,target:targetPath,rule:this.#serializeRule(winner),conditionMatched:winner.condition!=null},hasConditionalRules}}#resolveHookAccess(callerPath,hookPath,hookType,callerFilePath,targetFilePath,runtimeContext){if(callerPath==null)return{allowed:true,event:null,payload:null};const hookDecision=this.#evaluateHook(callerPath,hookPath,hookType,runtimeContext);if(hookDecision.matched){return{allowed:hookDecision.allowed,event:hookDecision.event,payload:hookDecision.payload}}const callDecision=this.#resolveAccess(callerPath,hookPath,callerFilePath,targetFilePath,runtimeContext,true);return{allowed:callDecision.allowed,event:callDecision.event,payload:callDecision.payload}}#evaluateHook(callerPath,hookPath,hookType,runtimeContext){const matches=[];for(const entry of this.#rules.values()){if(entry.hookType==null)continue;if(entry.hookType!=="hook"&&entry.hookType!==hookType)continue;const callerMatcher=this.#getCompiledPattern(entry.caller);const pathMatcher=this.#getCompiledPattern(entry.hookPathPattern);if(callerMatcher(callerPath)&&pathMatcher(hookPath)){matches.push(entry)}}const conditioned=matches.filter(entry=>this.#conditionMatches(entry,runtimeContext));if(conditioned.length===0){return{matched:false,allowed:false,event:null,payload:null}}const spec=e=>this.#patternSpecificity(e.caller,callerPath)+this.#patternSpecificity(e.hookPathPattern,hookPath)+(e.hookType==="hook"?0:1);conditioned.sort((a,b)=>{const specA=spec(a);const specB=spec(b);if(specA!==specB)return specB-specA;if(a.layerRank!==b.layerRank)return b.layerRank-a.layerRank;return b.registrationSeq-a.registrationSeq});const winner=conditioned[0];const allowed=winner.effect==="allow";return{matched:true,allowed,event:allowed?"permission:allowed":"permission:denied",payload:{caller:callerPath,target:`${hookPath}:${hookType}`,rule:this.#serializeRule(winner),conditionMatched:winner.condition!=null}}}#parseHookTarget(target){const lastColon=target.lastIndexOf(":");if(lastColon===-1)return null;const type=target.substring(lastColon+1);if(!HOOK_TARGET_TYPES.has(type))return null;const pathPattern=target.substring(0,lastColon);if(!pathPattern)return null;return{pathPattern,hookType:type}}#computeSpecificity(entry,callerPath,targetPath){return this.#patternSpecificity(entry.caller,callerPath)+this.#patternSpecificity(entry.target,targetPath)}#patternSpecificity(pattern,___path){if(!pattern.includes("*")&&!pattern.includes("?")&&!pattern.includes("{")){return 3}if(pattern.includes("**")){return 1}return 2}#getCompiledPattern(pattern){let matcher=this.#compiledCache.get(pattern);if(!matcher){matcher=compilePattern(pattern);this.#compiledCache.set(pattern,matcher)}return matcher}#clearCache(){this.#resolvedCache.clear()}#emitAuditEvent(event,payload){this.debug("permissions",{key:event==="permission:denied"?"DEBUG_PERMISSION_DENIED":event==="permission:allowed"?"DEBUG_PERMISSION_ALLOWED":event==="permission:self-bypass"?"DEBUG_PERMISSION_SELF_BYPASS":"DEBUG_PERMISSION_DEFAULT",...payload});const alwaysEmit=event==="permission:denied"||event==="permission:self-bypass";if(!alwaysEmit&&this.#audit!=="verbose")return;const lifecycle=this.slothlet.handlers?.lifecycle;if(lifecycle){lifecycle.emit(event,{...payload,timestamp:Date.now()})}}#serializeRule(entry){return{id:entry.id,caller:entry.caller,target:entry.target,effect:entry.effect,condition:entry.condition??null,ownerModuleID:entry.ownerModuleID,registeredAt:entry.registeredAt}}debug(category,data){this.slothlet.debug(category,data)}}export{PermissionManager};
|
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import{ComponentBase}from"#factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{resolveWrapper}from"#handlers/unified-wrapper";import{isFrameworkInternal}from"#handlers/framework-internals";import{compilePattern,expandBraces}from"@cldmv/slothlet/helpers/pattern-matcher";const ROOT_BUILTIN_NAMES=new Set(["shutdown","destroy"]);class RoutineManager extends ComponentBase{static slothletProperty="routineManager";constructor(slothlet){super(slothlet);this.raw=[];this.rawWrappers=new Map;this.recording=true;this.patternCache=new Map}reset(){this.raw=[];this.rawWrappers.clear();this.patternCache.clear()}get#routines(){return this.slothlet.config?.routines??[]}#moduleWrappers(moduleID){let inner=this.rawWrappers.get(moduleID);if(!inner){inner=new Map;this.rawWrappers.set(moduleID,inner)}return inner}#findRoutine(name){return this.#routines.find(routine=>routine.name===name)}#compile(pattern){let matcher=this.patternCache.get(pattern);if(!matcher){matcher=compilePattern(pattern);this.patternCache.set(pattern,matcher)}return matcher}#matches(routine,entry){const{name,recursive}=routine;if(name.startsWith("^")){return this.#compile(name.slice(1))(entry.apiPath)}const endpoint=this.slothlet.handlers.ownership?.getModuleEndpoint(entry.moduleID);if(endpoint===void 0)return false;let relative;if(endpoint==="."||endpoint==="")relative=entry.apiPath;else if(entry.apiPath===endpoint)relative="";else if(entry.apiPath.startsWith(`${endpoint}.`))relative=entry.apiPath.slice(endpoint.length+1);else return false;if(relative==="")return false;if(this.#compile(name)(relative))return true;return recursive&&this.#compile(`**.${name}`)(relative)}#requiresDescent(routine){if(routine.name.startsWith("^"))return true;if(routine.recursive)return true;return routine.name.includes(".")}#isCurrentOwner(entry){const ownership=this.slothlet.handlers.ownership;if(!ownership)return true;const owner=ownership.getCurrentOwner(entry.apiPath);if(!owner)return true;return owner.moduleID===entry.moduleID}#applyStackFilter(entries){if(this.slothlet.config?.stackRoutines)return entries;return entries.filter(entry=>this.#isCurrentOwner(entry))}#contributorsFor(name){const routine=this.#findRoutine(name);if(!routine)return[];return this.#applyStackFilter(this.raw.filter(entry=>this.#matches(routine,entry)))}#groupByPath(entries){const groups=new Map;for(const entry of entries){let group=groups.get(entry.apiPath);if(!group){group=[];groups.set(entry.apiPath,group)}group.push(entry)}return groups}onImplCreated(data){if(!this.recording)return;if(this.#routines.length===0)return;const apiPath=data?.apiPath;if(typeof apiPath!=="string"||apiPath.length===0)return;const moduleID=data.moduleID;const fn=data.wrapper?.__impl;const existingIndex=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(typeof fn==="function"&&fn.__slothletRoutineStack===true){return}if(typeof fn!=="function"){if(existingIndex!==-1)this.raw.splice(existingIndex,1);this.rawWrappers.get(moduleID)?.delete(apiPath);return}const entry={apiPath,moduleID,fn};if(existingIndex===-1)this.raw.push(entry);else this.raw[existingIndex]=entry;const wrapper=resolveWrapper(data.impl);if(wrapper)this.#moduleWrappers(moduleID).set(apiPath,wrapper);setImmediate(()=>{this.#reactivelyPatchStack(entry).catch(()=>{})})}async#reactivelyPatchStack(entry){if(this.slothlet.____buildDepth>0)return;const api=this.slothlet.api;if(!api)return;const lastDot=entry.apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":entry.apiPath.slice(0,lastDot);const key=lastDot===-1?entry.apiPath:entry.apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))return;let winner=null;let winnerGroup=null;const matchingRoutines=[];for(const routine of this.#routines){let group;try{group=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(routine,e)))}catch{continue}if(group.length===0)continue;matchingRoutines.push(routine);winner=routine;winnerGroup=group}if(!winner)return;const isCascadeSlot=parentPath===""&&key===winner.name;const contested=matchingRoutines.length>1;if(!isCascadeSlot&&winnerGroup.length<2&&!contested)return;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")return;if(this.slothlet.____buildDepth>0)return;if(this.slothlet.api!==api)return;const targetWrapper=resolveWrapper(target);if(targetWrapper&&targetWrapper.____slothletInternal?.invalid)return;let freshWinnerGroup;try{freshWinnerGroup=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(winner,e)))}catch{return}if(freshWinnerGroup.length===0)return;if(!isCascadeSlot&&freshWinnerGroup.length<2&&!contested)return;if(isCascadeSlot){let current2;try{current2=target[key]}catch{return}if(!(typeof current2==="function"&¤t2.__slothletRoutineCascade===true&¤t2.__slothletRoutineName===winner.name)){this.recording=false;try{target[key]=this.#buildCascadeCallable(winner.name)}catch{}finally{this.recording=true}}if(api.slothlet&&(typeof api.slothlet==="object"||typeof api.slothlet==="function")){let slothletCurrent;try{slothletCurrent=api.slothlet[key]}catch{return}if(!(typeof slothletCurrent==="function"&&slothletCurrent.__slothletRoutineCascade===true&&slothletCurrent.__slothletRoutineName===winner.name)){this.recording=false;try{api.slothlet[key]=this.#buildCascadeCallable(winner.name)}catch{}finally{this.recording=true}}}return}let current;try{current=target[key]}catch{return}if(typeof current==="function"&¤t.__slothletRoutineStack===true&¤t.__slothletRoutineName===winner.name){return}this.recording=false;try{target[key]=this.#buildStackedCallable(entry.apiPath,winner)}catch{}finally{this.recording=true}}onImplRemoved(data){const apiPath=data?.apiPath;const moduleID=data?.moduleID;if(typeof apiPath!=="string"||!moduleID)return;this.raw=this.raw.filter(e=>!(e.apiPath===apiPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(apiPath)}pruneSubtree(apiPath,moduleID){const prefix=`${apiPath}.`;const matching=this.raw.filter(e=>e.moduleID===moduleID&&(e.apiPath===apiPath||e.apiPath.startsWith(prefix)));if(matching.length===0)return;this.raw=this.raw.filter(e=>!matching.includes(e));const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const entry of matching){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(moduleWrappers.get(entry.apiPath));moduleWrappers.delete(entry.apiPath)}}pruneModule(moduleID){this.raw=this.raw.filter(e=>e.moduleID!==moduleID);const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const wrapper of moduleWrappers.values()){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(wrapper)}this.rawWrappers.delete(moduleID)}snapshotRawEntries(moduleID){const snapshot=new Map;const moduleWrappers=this.rawWrappers.get(moduleID);this.raw.forEach((entry,index)=>{if(entry.moduleID===moduleID)snapshot.set(entry.apiPath,{fn:entry.fn,index,wrapper:moduleWrappers?.get(entry.apiPath)})});return snapshot}snapshotRawEntry(apiPath,moduleID){const index=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(index===-1)return void 0;return{fn:this.raw[index].fn,index,wrapper:this.rawWrappers.get(moduleID)?.get(apiPath)}}revertRawEntry(apiPath,moduleID,priorEntry){const idx=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(priorEntry!==void 0){const entry={apiPath,moduleID,fn:priorEntry.fn};if(idx!==-1)this.raw[idx]=entry;else this.raw.splice(Math.min(priorEntry.index,this.raw.length),0,entry);if(priorEntry.wrapper)this.#moduleWrappers(moduleID).set(apiPath,priorEntry.wrapper);else this.rawWrappers.get(moduleID)?.delete(apiPath)}else{if(idx!==-1)this.raw.splice(idx,1);this.rawWrappers.get(moduleID)?.delete(apiPath)}}revertSpeculativeSubtree(api,moduleID,path,priorEntries,visited=new WeakSet){if(!api||typeof api!=="object"&&typeof api!=="function")return;if(visited.has(api)){return}visited.add(api);const revert=revertPath=>{const prior=priorEntries.get(revertPath);if(prior){const idx=this.raw.findIndex(e=>e.apiPath===revertPath&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:revertPath,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:revertPath,moduleID,fn:prior.fn});if(prior.wrapper)this.#moduleWrappers(moduleID).set(revertPath,prior.wrapper);else this.rawWrappers.get(moduleID)?.delete(revertPath)}else{this.raw=this.raw.filter(e=>!(e.apiPath===revertPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(revertPath)}};if(path){revert(path)}for(const[key,value]of Object.entries(api)){const skipProps=["__metadata","__type","_materialize","_impl","____slothletInternal"];if(skipProps.includes(key)){continue}const childPath=path?`${path}.${key}`:key;if(typeof value==="function"||value&&typeof value==="object"){revert(childPath);if(typeof value==="object"&&!Array.isArray(value)){this.revertSpeculativeSubtree(value,moduleID,childPath,priorEntries,visited)}}}}revertSpeculativeState(moduleID,priorEntries){const currentEntries=this.raw.filter(e=>e.moduleID===moduleID);const allPaths=new Set([...currentEntries.map(e=>e.apiPath),...priorEntries.keys()]);const moduleWrappers=this.rawWrappers.get(moduleID);for(const path of allPaths){const prior=priorEntries.get(path);const currentEntry=currentEntries.find(e=>e.apiPath===path);if(!currentEntry||currentEntry.fn!==prior?.fn){moduleWrappers?.get(path)?.___invalidate();if(prior?.wrapper)moduleWrappers?.set(path,prior.wrapper);else moduleWrappers?.delete(path)}if(prior){const idx=this.raw.findIndex(e=>e.apiPath===path&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:path,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:path,moduleID,fn:prior.fn})}else{this.raw=this.raw.filter(e=>!(e.apiPath===path&&e.moduleID===moduleID))}}}async#runEntries(apiPath,entries,args=[]){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const receiver=parentPath===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,parentPath);if(receiver===void 0){return{results:[],failures:[]}}const results=[];const failures=[];const contextManager=this.slothlet.contextManager;const instanceID=this.slothlet.instanceID;const canEnterExtent=contextManager&&typeof contextManager.runInContext==="function"&&contextManager.instances?.has?.(instanceID);for(const{moduleID,fn}of entries){try{if(canEnterExtent){const wrapper=this.rawWrappers.get(moduleID)?.get(apiPath);results.push(await contextManager.runInContext(instanceID,fn,receiver,args,wrapper,true))}else{results.push(await Reflect.apply(fn,receiver,args))}}catch(error){failures.push({apiPath,moduleID,error})}}return{results,failures}}#throwAggregate(failures){const failureEntries=failures.map(({apiPath,moduleID})=>{const entry={apiPath,moduleID};Object.defineProperty(entry,"toString",{value:()=>`${apiPath} (${moduleID})`,enumerable:false});return entry});Object.defineProperty(failureEntries,"toString",{value:()=>failureEntries.map(String).join(", "),enumerable:false});throw new SlothletError("ROUTINE_FAILED",{apiPath:failures[0].apiPath,moduleID:failures[0].moduleID,count:failures.length,failures:failureEntries},failures[0].error)}async runPath(apiPath,args=[],routine=null){if(!this.slothlet.api)return void 0;const pathEntries=this.raw.filter(entry=>entry.apiPath===apiPath);const scopedEntries=routine?pathEntries.filter(entry=>this.#matches(routine,entry)):pathEntries;const entries=this.#applyStackFilter(scopedEntries);const{results,failures}=await this.#runEntries(apiPath,entries,args);if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async#materializeTree(root){if(!root)return;const seen=new Set;const visit=async(obj,depth=0)=>{const objType=typeof obj;if(!obj||objType!=="object"&&objType!=="function"||depth>15||seen.has(obj))return;seen.add(obj);try{if(isFrameworkInternal(obj))return;const wrapper=resolveWrapper(obj);if(wrapper){if(wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return}}for(const key of Object.keys(wrapper)){if(!key.startsWith("____"))await visit(wrapper[key],depth+1)}return}for(const key of Object.keys(obj)){await visit(obj[key],depth+1)}}catch{}};const isApiRoot=root===this.slothlet.api;for(const key of Object.keys(root)){if(isApiRoot&&(key==="slothlet"||key==="shutdown"||key==="destroy"))continue;if(key.startsWith("____"))continue;await visit(root[key])}}async#materializeGlobPath(node,segments){if(node===null||node===void 0||segments.length===0)return;const nodeType=typeof node;if(nodeType!=="object"&&nodeType!=="function")return;const nodeWrapper=resolveWrapper(node);if(nodeWrapper&&nodeWrapper.____slothletInternal.mode==="lazy"&&!nodeWrapper.____slothletInternal.state.materialized){try{await nodeWrapper._materialize()}catch{return}}const[segment,...rest]=segments;if(segment==="**"){await this.#materializeTree(node);return}if(!/[*?]/.test(segment)){let child;try{child=node[segment]}catch{return}await this.#materializeGlobPath(child,rest);return}let keys;try{keys=Object.keys(node)}catch{return}const matches=this.#compile(segment);for(const key of keys){if(!matches(key))continue;let child;try{child=node[key]}catch{continue}await this.#materializeGlobPath(child,rest)}}async#materializeFor(routine){if(!this.#requiresDescent(routine))return;if(routine.name.startsWith("^")){await this.#materializeTree(this.slothlet.api);return}const ownership=this.slothlet.handlers.ownership;const endpoints=ownership?new Set(ownership.moduleEndpoints.values()):new Set;if(!routine.recursive&&!routine.name.startsWith("!")){const segmentChains=expandBraces(routine.name).map(alternative=>alternative.split("."));for(const endpoint of endpoints){const mountRoot=endpoint==="."||endpoint===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,endpoint);if(mountRoot===null||mountRoot===void 0)continue;for(const segments of segmentChains){await this.#materializeGlobPath(mountRoot,segments)}}return}if(endpoints.has(".")||endpoints.has("")){await this.#materializeTree(this.slothlet.api);return}for(const endpoint of endpoints){const mountRoot=await this.#resolveContainer(this.slothlet.api,endpoint);await this.#materializeTree(mountRoot)}}#orderPaths(apiPaths,order){if(order!=="depth")return apiPaths;return apiPaths.map((apiPath,index)=>({apiPath,index,depth:apiPath.split(".").length})).sort((a,b)=>b.depth-a.depth||a.index-b.index).map(entry=>entry.apiPath)}async runCascade(name,args=[],skipMaterialize=false){if(!this.slothlet.api)return void 0;const routine=this.#findRoutine(name);if(!routine)return void 0;if(!skipMaterialize)await this.#materializeFor(routine);const groups=this.#groupByPath(this.#contributorsFor(name));const orderedPaths=this.#orderPaths([...groups.keys()],routine.order);const results=[];const failures=[];for(const apiPath of orderedPaths){const outcome=await this.#runEntries(apiPath,groups.get(apiPath),args);results.push(outcome.results.length===1?outcome.results[0]:outcome.results);failures.push(...outcome.failures)}if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async#runModeRoutines(mode){if(!this.slothlet.config?.autoRoutines)return;const routines=this.#routines.filter(routine=>routine.mode===mode);if(routines.length===0)return;for(const routine of routines){await this.#materializeFor(routine)}await this.rebuildStacks(this.slothlet.api);for(const routine of routines){await this.runCascade(routine.name,[],true)}}async runShutdownModeRoutines(){return this.#runModeRoutines("shutdown")}async runDestroyModeRoutines(){return this.#runModeRoutines("destroy")}async runStartupModeRoutines(){return this.#runModeRoutines("startup")}async#resolveContainer(api,path){if(path==="")return api;let node=api;for(const part of path.split(".")){if(node===null||node===void 0)return void 0;try{node=node[part]}catch{return void 0}const wrapper=resolveWrapper(node);if(wrapper&&wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return void 0}}}return node}#buildStackedCallable(apiPath,routine){const manager=this;const stacked=async function slothletRoutineStack(...args){return manager.runPath(apiPath,args,routine)};Object.defineProperty(stacked,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(stacked,"__slothletRoutineName",{value:routine.name,enumerable:false});return stacked}#buildCascadeCallable(name){const manager=this;const cascade=async function slothletRoutineCascade(...args){return manager.runCascade(name,args)};Object.defineProperty(cascade,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineCascade",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineName",{value:name,enumerable:false});return cascade}async rebuildStacks(api){if(!api||typeof api!=="object"&&typeof api!=="function")return;this.recording=false;try{for(const routine of this.#routines){const groups=this.#groupByPath(this.#contributorsFor(routine.name));for(const apiPath of groups.keys()){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const key=lastDot===-1?apiPath:apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))continue;if(parentPath===""&&key===routine.name)continue;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")continue;try{target[key]=this.#buildStackedCallable(apiPath,routine)}catch{}}if(ROOT_BUILTIN_NAMES.has(routine.name))continue;const cascade=this.#buildCascadeCallable(routine.name);try{api[routine.name]=cascade}catch{}if(api.slothlet&&(typeof api.slothlet==="object"||typeof api.slothlet==="function")){try{api.slothlet[routine.name]=cascade}catch{}}}}finally{this.recording=true}}}export{RoutineManager};
|
|
17
|
+
import{ComponentBase}from"#factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{resolveWrapper}from"#handlers/unified-wrapper";import{isFrameworkInternal}from"#handlers/framework-internals";import{compilePattern,expandBraces}from"@cldmv/slothlet/helpers/pattern-matcher";const ROOT_BUILTIN_NAMES=new Set(["shutdown","destroy"]);class RoutineManager extends ComponentBase{static slothletProperty="routineManager";constructor(slothlet){super(slothlet);this.raw=[];this.rawWrappers=new Map;this.recording=true;this.patternCache=new Map}reset(){this.raw=[];this.rawWrappers.clear();this.patternCache.clear()}get#routines(){return this.slothlet.config?.routines??[]}#moduleWrappers(moduleID){let inner=this.rawWrappers.get(moduleID);if(!inner){inner=new Map;this.rawWrappers.set(moduleID,inner)}return inner}#findRoutine(name){return this.#routines.find(routine=>routine.name===name)}#compile(pattern){let matcher=this.patternCache.get(pattern);if(!matcher){matcher=compilePattern(pattern);this.patternCache.set(pattern,matcher)}return matcher}#matches(routine,entry){const{name,recursive}=routine;if(name.startsWith("^")){return this.#compile(name.slice(1))(entry.apiPath)}const endpoint=this.slothlet.handlers.ownership?.getModuleEndpoint(entry.moduleID);if(endpoint===void 0)return false;let relative;if(endpoint==="."||endpoint==="")relative=entry.apiPath;else if(entry.apiPath===endpoint)relative="";else if(entry.apiPath.startsWith(`${endpoint}.`))relative=entry.apiPath.slice(endpoint.length+1);else return false;if(relative==="")return false;if(this.#compile(name)(relative))return true;return recursive&&this.#compile(`**.${name}`)(relative)}#requiresDescent(routine){if(routine.name.startsWith("^"))return true;if(routine.recursive)return true;return routine.name.includes(".")}#isCurrentOwner(entry){const ownership=this.slothlet.handlers.ownership;if(!ownership)return true;const owner=ownership.getCurrentOwner(entry.apiPath);if(!owner)return true;return owner.moduleID===entry.moduleID}#applyStackFilter(entries){if(this.slothlet.config?.stackRoutines)return entries;return entries.filter(entry=>this.#isCurrentOwner(entry))}#contributorsFor(name){const routine=this.#findRoutine(name);if(!routine)return[];return this.#applyStackFilter(this.raw.filter(entry=>this.#matches(routine,entry)))}#groupByPath(entries){const groups=new Map;for(const entry of entries){let group=groups.get(entry.apiPath);if(!group){group=[];groups.set(entry.apiPath,group)}group.push(entry)}return groups}onImplCreated(data){if(!this.recording)return;if(this.#routines.length===0)return;const apiPath=data?.apiPath;if(typeof apiPath!=="string"||apiPath.length===0)return;const moduleID=data.moduleID;const fn=data.wrapper?.__impl;const existingIndex=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(typeof fn==="function"&&fn.__slothletRoutineStack===true){return}if(typeof fn!=="function"){if(existingIndex!==-1)this.raw.splice(existingIndex,1);this.rawWrappers.get(moduleID)?.delete(apiPath);return}const entry={apiPath,moduleID,fn};if(existingIndex===-1)this.raw.push(entry);else this.raw[existingIndex]=entry;const wrapper=resolveWrapper(data.__wrapperRef);if(wrapper)this.#moduleWrappers(moduleID).set(apiPath,wrapper);setImmediate(()=>{this.#reactivelyPatchStack(entry).catch(()=>{})})}async#reactivelyPatchStack(entry){if(this.slothlet.____buildDepth>0)return;const api=this.slothlet.api;if(!api)return;const lastDot=entry.apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":entry.apiPath.slice(0,lastDot);const key=lastDot===-1?entry.apiPath:entry.apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))return;let winner=null;let winnerGroup=null;const matchingRoutines=[];for(const routine of this.#routines){let group;try{group=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(routine,e)))}catch{continue}if(group.length===0)continue;matchingRoutines.push(routine);winner=routine;winnerGroup=group}if(!winner)return;const isCascadeSlot=parentPath===""&&key===winner.name;const contested=matchingRoutines.length>1;if(!isCascadeSlot&&winnerGroup.length<2&&!contested)return;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")return;if(this.slothlet.____buildDepth>0)return;if(this.slothlet.api!==api)return;const targetWrapper=resolveWrapper(target);if(targetWrapper&&targetWrapper.____slothletInternal?.invalid)return;let freshWinnerGroup;try{freshWinnerGroup=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(winner,e)))}catch{return}if(freshWinnerGroup.length===0)return;if(!isCascadeSlot&&freshWinnerGroup.length<2&&!contested)return;if(isCascadeSlot){if(!winner.cascade)return;let current2;try{current2=target[key]}catch{return}if(!(typeof current2==="function"&¤t2.__slothletRoutineCascade===true&¤t2.__slothletRoutineName===winner.name)){this.recording=false;try{target[key]=this.#buildCascadeCallable(winner.name)}catch{}finally{this.recording=true}}return}let current;try{current=target[key]}catch{return}if(typeof current==="function"&¤t.__slothletRoutineStack===true&¤t.__slothletRoutineName===winner.name){return}this.recording=false;try{target[key]=this.#buildStackedCallable(entry.apiPath,winner)}catch{}finally{this.recording=true}}onImplRemoved(data){const apiPath=data?.apiPath;const moduleID=data?.moduleID;if(typeof apiPath!=="string"||!moduleID)return;this.raw=this.raw.filter(e=>!(e.apiPath===apiPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(apiPath)}pruneSubtree(apiPath,moduleID){const prefix=`${apiPath}.`;const matching=this.raw.filter(e=>e.moduleID===moduleID&&(e.apiPath===apiPath||e.apiPath.startsWith(prefix)));if(matching.length===0)return;this.raw=this.raw.filter(e=>!matching.includes(e));const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const entry of matching){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(moduleWrappers.get(entry.apiPath));moduleWrappers.delete(entry.apiPath)}}pruneModule(moduleID){this.raw=this.raw.filter(e=>e.moduleID!==moduleID);const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const wrapper of moduleWrappers.values()){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(wrapper)}this.rawWrappers.delete(moduleID)}snapshotRawEntries(moduleID){const snapshot=new Map;const moduleWrappers=this.rawWrappers.get(moduleID);this.raw.forEach((entry,index)=>{if(entry.moduleID===moduleID)snapshot.set(entry.apiPath,{fn:entry.fn,index,wrapper:moduleWrappers?.get(entry.apiPath)})});return snapshot}snapshotRawEntry(apiPath,moduleID){const index=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(index===-1)return void 0;return{fn:this.raw[index].fn,index,wrapper:this.rawWrappers.get(moduleID)?.get(apiPath)}}revertRawEntry(apiPath,moduleID,priorEntry){const idx=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(priorEntry!==void 0){const entry={apiPath,moduleID,fn:priorEntry.fn};if(idx!==-1)this.raw[idx]=entry;else this.raw.splice(Math.min(priorEntry.index,this.raw.length),0,entry);if(priorEntry.wrapper)this.#moduleWrappers(moduleID).set(apiPath,priorEntry.wrapper);else this.rawWrappers.get(moduleID)?.delete(apiPath)}else{if(idx!==-1)this.raw.splice(idx,1);this.rawWrappers.get(moduleID)?.delete(apiPath)}}revertSpeculativeSubtree(api,moduleID,path,priorEntries,visited=new WeakSet){if(!api||typeof api!=="object"&&typeof api!=="function")return;if(visited.has(api)){return}visited.add(api);const revert=revertPath=>{const prior=priorEntries.get(revertPath);if(prior){const idx=this.raw.findIndex(e=>e.apiPath===revertPath&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:revertPath,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:revertPath,moduleID,fn:prior.fn});if(prior.wrapper)this.#moduleWrappers(moduleID).set(revertPath,prior.wrapper);else this.rawWrappers.get(moduleID)?.delete(revertPath)}else{this.raw=this.raw.filter(e=>!(e.apiPath===revertPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(revertPath)}};if(path){revert(path)}for(const[key,value]of Object.entries(api)){const skipProps=["__metadata","__type","_materialize","_impl","____slothletInternal"];if(skipProps.includes(key)){continue}const childPath=path?`${path}.${key}`:key;if(typeof value==="function"||value&&typeof value==="object"){revert(childPath);if(typeof value==="object"&&!Array.isArray(value)){this.revertSpeculativeSubtree(value,moduleID,childPath,priorEntries,visited)}}}}revertSpeculativeState(moduleID,priorEntries){const currentEntries=this.raw.filter(e=>e.moduleID===moduleID);const allPaths=new Set([...currentEntries.map(e=>e.apiPath),...priorEntries.keys()]);const moduleWrappers=this.rawWrappers.get(moduleID);for(const path of allPaths){const prior=priorEntries.get(path);const currentEntry=currentEntries.find(e=>e.apiPath===path);if(!currentEntry||currentEntry.fn!==prior?.fn){moduleWrappers?.get(path)?.___invalidate();if(prior?.wrapper)moduleWrappers?.set(path,prior.wrapper);else moduleWrappers?.delete(path)}if(prior){const idx=this.raw.findIndex(e=>e.apiPath===path&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:path,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:path,moduleID,fn:prior.fn})}else{this.raw=this.raw.filter(e=>!(e.apiPath===path&&e.moduleID===moduleID))}}}async#runEntries(apiPath,entries,args=[]){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const receiver=parentPath===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,parentPath);if(receiver===void 0){return{results:[],failures:[]}}const results=[];const failures=[];const contextManager=this.slothlet.contextManager;const instanceID=this.slothlet.instanceID;const canEnterExtent=contextManager&&typeof contextManager.runInContext==="function"&&contextManager.instances?.has?.(instanceID);for(const{moduleID,fn}of entries){try{if(canEnterExtent){const wrapper=this.rawWrappers.get(moduleID)?.get(apiPath);results.push(await contextManager.runInContext(instanceID,fn,receiver,args,wrapper,true))}else{results.push(await Reflect.apply(fn,receiver,args))}}catch(error){failures.push({apiPath,moduleID,error})}}return{results,failures}}#throwAggregate(failures){const failureEntries=failures.map(({apiPath,moduleID})=>{const entry={apiPath,moduleID};Object.defineProperty(entry,"toString",{value:()=>`${apiPath} (${moduleID})`,enumerable:false});return entry});Object.defineProperty(failureEntries,"toString",{value:()=>failureEntries.map(String).join(", "),enumerable:false});throw new SlothletError("ROUTINE_FAILED",{apiPath:failures[0].apiPath,moduleID:failures[0].moduleID,count:failures.length,failures:failureEntries},failures[0].error)}async runPath(apiPath,args=[],routine=null){if(!this.slothlet.api)return void 0;const pathEntries=this.raw.filter(entry=>entry.apiPath===apiPath);const scopedEntries=routine?pathEntries.filter(entry=>this.#matches(routine,entry)):pathEntries;const entries=this.#applyStackFilter(scopedEntries);const{results,failures}=await this.#runEntries(apiPath,entries,args);if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async runPathFor(apiPath,key,args=[],routine=null){if(!this.slothlet.api)return void 0;const pathEntries=this.raw.filter(entry=>entry.apiPath===apiPath&&entry.moduleID===key);const entries=routine?pathEntries.filter(entry=>this.#matches(routine,entry)):pathEntries;if(entries.length===0){const available=this.contributorsAt(apiPath,routine);throw new SlothletError("INVALID_ARGUMENT",{argument:"key",expected:`a moduleID contributing "${routine?.name??"the routine"}" at "${apiPath}"${available.length?` (contributors: ${available.join(", ")})`:" (no contributors at this path)"}`,received:String(key),validationError:true},null,{validationError:true})}const{results,failures}=await this.#runEntries(apiPath,entries,args);if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}contributorsAt(apiPath,routine=null){const pathEntries=this.raw.filter(entry=>entry.apiPath===apiPath);const entries=routine?pathEntries.filter(entry=>this.#matches(routine,entry)):pathEntries;return entries.map(entry=>entry.moduleID)}async#materializeTree(root){if(!root)return;const seen=new Set;const visit=async(obj,depth=0)=>{const objType=typeof obj;if(!obj||objType!=="object"&&objType!=="function"||depth>15||seen.has(obj))return;seen.add(obj);try{if(isFrameworkInternal(obj))return;const wrapper=resolveWrapper(obj);if(wrapper){if(wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return}}for(const key of Object.keys(wrapper)){if(!key.startsWith("____"))await visit(wrapper[key],depth+1)}return}for(const key of Object.keys(obj)){await visit(obj[key],depth+1)}}catch{}};const isApiRoot=root===this.slothlet.api;for(const key of Object.keys(root)){if(isApiRoot&&(key==="slothlet"||key==="shutdown"||key==="destroy"))continue;if(key.startsWith("____"))continue;await visit(root[key])}}async#materializeGlobPath(node,segments){if(node===null||node===void 0||segments.length===0)return;const nodeType=typeof node;if(nodeType!=="object"&&nodeType!=="function")return;const nodeWrapper=resolveWrapper(node);if(nodeWrapper&&nodeWrapper.____slothletInternal.mode==="lazy"&&!nodeWrapper.____slothletInternal.state.materialized){try{await nodeWrapper._materialize()}catch{return}}const[segment,...rest]=segments;if(segment==="**"){await this.#materializeTree(node);return}if(!/[*?]/.test(segment)){let child;try{child=node[segment]}catch{return}await this.#materializeGlobPath(child,rest);return}let keys;try{keys=Object.keys(node)}catch{return}const matches=this.#compile(segment);for(const key of keys){if(!matches(key))continue;let child;try{child=node[key]}catch{continue}await this.#materializeGlobPath(child,rest)}}async#materializeFor(routine){if(!this.#requiresDescent(routine))return;if(routine.name.startsWith("^")){await this.#materializeTree(this.slothlet.api);return}const ownership=this.slothlet.handlers.ownership;const endpoints=ownership?new Set(ownership.moduleEndpoints.values()):new Set;if(!routine.recursive&&!routine.name.startsWith("!")){const segmentChains=expandBraces(routine.name).map(alternative=>alternative.split("."));for(const endpoint of endpoints){const mountRoot=endpoint==="."||endpoint===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,endpoint);if(mountRoot===null||mountRoot===void 0)continue;for(const segments of segmentChains){await this.#materializeGlobPath(mountRoot,segments)}}return}if(endpoints.has(".")||endpoints.has("")){await this.#materializeTree(this.slothlet.api);return}for(const endpoint of endpoints){const mountRoot=await this.#resolveContainer(this.slothlet.api,endpoint);await this.#materializeTree(mountRoot)}}#orderPaths(apiPaths,order){if(order!=="depth")return apiPaths;return apiPaths.map((apiPath,index)=>({apiPath,index,depth:apiPath.split(".").length})).sort((a,b)=>b.depth-a.depth||a.index-b.index).map(entry=>entry.apiPath)}async runCascade(name,args=[],skipMaterialize=false){if(!this.slothlet.api)return void 0;const routine=this.#findRoutine(name);if(!routine)return void 0;if(!skipMaterialize)await this.#materializeFor(routine);const groups=this.#groupByPath(this.#contributorsFor(name));const orderedPaths=this.#orderPaths([...groups.keys()],routine.order);const results=[];const failures=[];for(const apiPath of orderedPaths){const outcome=await this.#runEntries(apiPath,groups.get(apiPath),args);results.push(outcome.results.length===1?outcome.results[0]:outcome.results);failures.push(...outcome.failures)}if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async#runModeRoutines(mode){if(!this.slothlet.config?.autoRoutines)return;const routines=this.#routines.filter(routine=>routine.mode===mode);if(routines.length===0)return;for(const routine of routines){await this.#materializeFor(routine)}await this.rebuildStacks(this.slothlet.api);for(const routine of routines){await this.runCascade(routine.name,[],true)}}async runShutdownModeRoutines(){return this.#runModeRoutines("shutdown")}async runDestroyModeRoutines(){return this.#runModeRoutines("destroy")}async runStartupModeRoutines(){return this.#runModeRoutines("startup")}async#resolveContainer(api,path){if(path==="")return api;let node=api;for(const part of path.split(".")){if(node===null||node===void 0)return void 0;try{node=node[part]}catch{return void 0}const wrapper=resolveWrapper(node);if(wrapper&&wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return void 0}}}return node}#buildStackedCallable(apiPath,routine){const manager=this;const stacked=async function slothletRoutineStack(...args){return manager.runPath(apiPath,args,routine)};Object.defineProperty(stacked,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(stacked,"__slothletRoutineName",{value:routine.name,enumerable:false});Object.defineProperty(stacked,"for",{value:key=>async function slothletRoutineFor(...args){return manager.runPathFor(apiPath,key,args,routine)},enumerable:false});Object.defineProperty(stacked,"contributors",{get:()=>manager.contributorsAt(apiPath,routine),enumerable:false});return stacked}#buildCascadeCallable(name){const manager=this;const cascade=async function slothletRoutineCascade(...args){return manager.runCascade(name,args)};Object.defineProperty(cascade,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineCascade",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineName",{value:name,enumerable:false});return cascade}async rebuildStacks(api){if(!api||typeof api!=="object"&&typeof api!=="function")return;this.recording=false;try{for(const routine of this.#routines){const groups=this.#groupByPath(this.#contributorsFor(routine.name));for(const apiPath of groups.keys()){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const key=lastDot===-1?apiPath:apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))continue;if(parentPath===""&&key===routine.name)continue;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")continue;try{target[key]=this.#buildStackedCallable(apiPath,routine)}catch{}}if(ROOT_BUILTIN_NAMES.has(routine.name))continue;if(!routine.cascade)continue;const cascade=this.#buildCascadeCallable(routine.name);try{api[routine.name]=cascade}catch{}}}finally{this.recording=true}}}export{RoutineManager};
|