@alwatr/synapse 9.3.0 → 9.4.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.
@@ -0,0 +1,2 @@
1
+ export declare function autoDestructDirectives(): void;
2
+ //# sourceMappingURL=auto-destruct.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-destruct.d.ts","sourceRoot":"","sources":["../src/auto-destruct.ts"],"names":[],"mappings":"AAEA,wBAAgB,sBAAsB,IAAI,IAAI,CAO7C"}
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,GAAE,OAAO,GAAG,QAAwB,GAAG,IAAI,CA6BzF"}
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,GAAE,OAAO,GAAG,QAAwB,GAAG,IAAI,CAiDzF"}
@@ -45,6 +45,7 @@ export declare abstract class DirectiveBase {
45
45
  * A list of callback functions to be executed when the directive is destroyed.
46
46
  */
47
47
  private readonly cleanupTaskList__;
48
+ readonly index: number;
48
49
  /**
49
50
  * Initializes the directive. This constructor is called by the Synapse bootstrap process and should not be
50
51
  * overridden in subclasses.
@@ -76,10 +77,10 @@ export declare abstract class DirectiveBase {
76
77
  *
77
78
  * @example
78
79
  * ```ts
79
- * this.dispatch_('user-action', {action: 'save', id: 123});
80
+ * this.dispatch('user-action', {action: 'save', id: 123});
80
81
  * ```
81
82
  */
82
- protected dispatch_(eventName: string, detail?: unknown): void;
83
+ dispatch(eventName: string, detail?: unknown): void;
83
84
  /**
84
85
  * Registers a task to be executed when the directive is destroyed.
85
86
  * Follows the `on[Event]` pattern, similar to `onClick`.
@@ -94,7 +95,7 @@ export declare abstract class DirectiveBase {
94
95
  * );
95
96
  * ```
96
97
  */
97
- protected onDestroy_(task: NoopFunc): void;
98
+ onDestroy(task: (this: this) => Awaitable<void>): void;
98
99
  /**
99
100
  * Cleans up the directive's resources.
100
101
  *
@@ -102,6 +103,15 @@ export declare abstract class DirectiveBase {
102
103
  * helping with garbage collection. It can be extended by subclasses to perform additional cleanup,
103
104
  * such as removing event listeners.
104
105
  */
105
- protected destroy_(): Awaitable<void>;
106
+ destroy(): Awaitable<void>;
107
+ /**
108
+ * Automatically destroys the directive if its associated element is no longer connected to the DOM.
109
+ *
110
+ * This method can be called periodically (e.g., in a `MutationObserver` or a cleanup loop) to ensure that
111
+ * directives are properly cleaned up when their elements are removed from the DOM.
112
+ *
113
+ * **Note:** This method does not automatically run; you must call it as needed to check for disconnected elements.
114
+ */
115
+ autoDestroy(): boolean;
106
116
  }
107
117
  //# sourceMappingURL=directive-class.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"directive-class.d.ts","sourceRoot":"","sources":["../src/directive-class.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,8BAAsB,aAAa;IACjC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,SAAS,SAAC;IAE7B;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,wCAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAEzC;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAkB;IAEpD;;;;;;;;;OASG;gBACS,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM;IAalD;;;;;;;OAOG;IACH,SAAS,CAAC,KAAK,IAAI,SAAS,CAAC,IAAI,CAAC;IAOlC;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI;IAK9D;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAK1C;;;;;;OAMG;IACH,SAAS,CAAC,QAAQ,IAAI,SAAS,CAAC,IAAI,CAAC;CAqBtC"}
1
+ {"version":3,"file":"directive-class.d.ts","sourceRoot":"","sources":["../src/directive-class.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,8BAAsB,aAAa;IACjC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,SAAS,SAAC;IAE7B;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,wCAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAEzC;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAkB;IAEpD,SAAgB,KAAK,EAAE,MAAM,CAAC;IAE9B;;;;;;;;;OASG;gBACS,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM;IAiBlD;;;;;;;OAOG;IACH,SAAS,CAAC,KAAK,IAAI,SAAS,CAAC,IAAI,CAAC;IAOlC;;;;;;;;;;;;;OAaG;IACI,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI;IAK1D;;;;;;;;;;;;;OAaG;IACI,SAAS,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI;IAK7D;;;;;;OAMG;IACI,OAAO,IAAI,SAAS,CAAC,IAAI,CAAC;IAqBjC;;;;;;;OAOG;IACI,WAAW,IAAI,OAAO;CAQ9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"directive-decorator.d.ts","sourceRoot":"","sources":["../src/directive-decorator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;GAGG;AACH,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,IAAI,KAAK,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC,CAAC;AAE9H;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAQvB,aAAa,oBAAoB,EAAE,SAAS,qBAAqB,KAAG,IAAI,CAU1F"}
1
+ {"version":3,"file":"directive-decorator.d.ts","sourceRoot":"","sources":["../src/directive-decorator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;AAExD;;;GAGG;AACH,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,IAAI,KAC1E,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,MAAM,KACb,CAAC,CAAC;AAEP;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAQvB,aAAa,oBAAoB,EAAE,SAAS,qBAAqB,KAAG,IAAI,CAY1F"}
package/dist/lib.d.ts CHANGED
@@ -1,13 +1,23 @@
1
1
  import type { DirectiveConstructor } from './directive-decorator.js';
2
+ import type { DirectiveBase } from './directive-class.js';
2
3
  /**
3
4
  * Alwatr Synapse Logger.
4
5
  */
5
6
  export declare const logger: import("@alwatr/logger").AlwatrLogger;
6
7
  /**
7
- * The registry for all directives.
8
+ * A Map to store registered directives.
9
+ * The key is the directive selector (e.g., '.my-button'), and the value is the constructor class for that directive.
10
+ * This registry is populated by the `@Directive` decorator when directives are defined.
11
+ * The `bootstrapDirectives` function uses this registry to find and initialize directives on the page.
8
12
  */
9
- export declare const directiveRegistry_: {
10
- selector: string;
11
- constructor: DirectiveConstructor;
12
- }[];
13
+ export declare const directiveRegistry_: Map<string, DirectiveConstructor>;
14
+ /**
15
+ * A WeakMap to track which directives have been initialized on which elements.
16
+ * The key is the DOM element, and the value is a Set of directive selectors that have been initialized on that element.
17
+ * This allows us to prevent multiple initializations of the same directive on the same element, and also to clean up the tracking when elements are destroyed.
18
+ * The `bootstrapDirectives` function updates this WeakMap as it initializes directives, and checks it to avoid re-initialization.
19
+ */
20
+ export declare const initializedDirectives_: WeakMap<Element, Set<string>>;
21
+ export declare const directiveInstanceRegistry_: Set<DirectiveBase>;
22
+ export declare const finalizationRegistry: FinalizationRegistry<unknown> | null;
13
23
  //# sourceMappingURL=lib.d.ts.map
package/dist/lib.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,0BAA0B,CAAC;AAEnE;;GAEG;AACH,eAAO,MAAM,MAAM,uCAAiC,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,oBAAoB,CAAA;CAAC,EAAO,CAAC"}
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,0BAA0B,CAAC;AACnE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,MAAM,uCAAiC,CAAC;AAErD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,mCAA0C,CAAC;AAE1E;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,+BAAsC,CAAC;AAE1E,eAAO,MAAM,0BAA0B,oBAA2B,CAAC;AAEnE,eAAO,MAAM,oBAAoB,sCAKzB,CAAC"}
package/dist/main.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './bootstrap.js';
2
+ export * from './auto-destruct.js';
2
3
  export * from './directive-decorator.js';
3
4
  export * from './directive-class.js';
4
5
  export * from './query-decorator.js';
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
- /* 📦 @alwatr/synapse v9.3.0 */
2
- import{createLogger as V}from"@alwatr/logger";var M=V("alwatr/synapse"),P=[];var U="synapse";function W(B=document.body){if(M.logMethod?.("bootstrapDirectives"),document.readyState==="loading"){M.incident?.("bootstrapDirectives","dom_not_ready","Delaying directive initialization until DOM is ready"),document.addEventListener("DOMContentLoaded",()=>{W(B)},{once:!0});return}for(let{selector:C,constructor:H}of P)try{let O=`${C}:not([${U}])`,F=B.querySelectorAll(O);if(F.length===0)continue;M.logOther?.(`Found ${F.length} new element(s) for directive "${C}"`),F.forEach((G)=>{G.setAttribute(U,""),new H(G,C)})}catch(O){M.error("bootstrapDirectives","directive_instantiation_error",O,{selector:C})}}function S(B){return M.logMethodArgs?.("@directive",B),function(C,H){if(H.kind!=="class")throw Error("@directive can only be used on classes");P.push({selector:B,constructor:C})}}import{delay as X}from"@alwatr/delay";import{createLogger as Y}from"@alwatr/logger";class Z{selector_;logger_;element_;cleanupTaskList__=[];constructor(B,C){this.logger_=Y(`directive:${C}`),this.logger_.logMethodArgs?.("new",{selector:C,element:B}),this.selector_=C,this.element_=B,(async()=>{await X.nextMicrotask(),await this.init_()})()}init_(){this.logger_.logMethod?.("init_"),this.update_?.()}dispatch_(B,C){this.logger_.logMethodArgs?.("dispatch_",{eventName:B,detail:C}),this.element_.dispatchEvent(new CustomEvent(B,{detail:C,bubbles:!0}))}onDestroy_(B){this.logger_.logMethod?.("onDestroy_"),this.cleanupTaskList__.push(B)}destroy_(){if(this.logger_.logMethod?.("destroy_"),this.cleanupTaskList__.length>0){for(let B of this.cleanupTaskList__)try{B.call(this)}catch(C){this.logger_.error("destroy_","error_in_destroy_callback",C)}this.cleanupTaskList__.length=0}this.element_?.remove(),this.element_=null}}function k(B,C=!0,H){return function(O,F){if(F.kind!=="accessor")throw Error('@query can only be used with the "accessor" keyword');let G=Symbol(`${String(F.name)}__`);return{get(){let J=this[G];if(J===void 0||C===!1){let Q=H??this.element_;J=this[G]=Q.querySelector(B)}return J}}}}function z(B,C=!0,H){return function(O,F){if(F.kind!=="accessor")throw Error('@queryAll can only be used with the "accessor" keyword');let G=Symbol(`${String(F.name)}__`);return{get(){let J=this[G];if(J===void 0||C===!1){let Q=H??this.element_;J=this[G]=Q.querySelectorAll(B)}return J}}}}export{z as queryAll,k as query,S as directive,W as bootstrapDirectives,Z as DirectiveBase};
1
+ /* 📦 @alwatr/synapse v9.4.0 */
2
+ import{createLogger as V}from"@alwatr/logger";var H=V("alwatr/synapse"),Y=new Map,Z=new WeakMap,$=new Set,F=typeof FinalizationRegistry<"u"?new FinalizationRegistry((B)=>{H.logOther?.(`Directive ${B} has been garbage collected successfully.`)}):null;function W(B=document.body){if(H.logMethod?.("bootstrapDirectives"),document.readyState==="loading"){H.incident?.("bootstrapDirectives","dom_not_ready","Delaying directive initialization until DOM is ready"),document.addEventListener("DOMContentLoaded",()=>{W(B)},{once:!0});return}for(let[G,U]of Y)try{let X=B.querySelectorAll(G);if(X.length===0){H.logOther?.("no_elements_found",{selector:G});continue}for(let P of X){let Q=Z.get(P);if(Q?.has(G)){H.incident?.("bootstrapDirectives","directive_already_initialized",{selector:G,element:P});continue}if(!Q)Q=new Set([G]),Z.set(P,Q);try{let J=new U(P,G);Q.add(G),J.onDestroy(f),$.add(J)}catch(J){H.error("bootstrapDirectives","directive_instantiation_error",{selector:G,element:P},J)}}}catch(X){H.error("bootstrapDirectives","directive_instantiation_error",X,{selector:G})}}function f(){let B=Z.get(this.element_);if(B){if(B.delete(this.selector_),B.size===0)Z.delete(this.element_)}}function O(){H.logMethod?.("autoDestructDirectives");for(let B of $)if(B.autoDestroy())$.delete(B)}function k(B){return H.logMethodArgs?.("@directive",B),function(G,U){if(U.kind!=="class")throw Error("@directive can only be used on classes");if(Y.has(B)){H.accident("@directive","duplicate_selector_registration",{selector:B});return}Y.set(B,G)}}import{delay as A}from"@alwatr/delay";import{createLogger as C}from"@alwatr/logger";var j=new Map;function E(B){let G=j.get(B)??0;return j.set(B,G+1),G}class K{selector_;logger_;element_;cleanupTaskList__=[];index;constructor(B,G){this.index=E(G);let U=`directive:${G}/${this.index}`;this.logger_=C(U),this.logger_.logMethodArgs?.("new",{selector:G,element:B}),this.selector_=G,this.element_=B,F?.register(this,U),(async()=>{await A.nextMicrotask(),await this.init_()})()}init_(){this.logger_.logMethod?.("init_"),this.update_?.()}dispatch(B,G){this.logger_.logMethodArgs?.("dispatch",{eventName:B,detail:G}),this.element_.dispatchEvent(new CustomEvent(B,{detail:G,bubbles:!0}))}onDestroy(B){this.logger_.logMethod?.("onDestroy"),this.cleanupTaskList__.push(B)}destroy(){if(this.logger_.logMethod?.("destroy"),this.cleanupTaskList__.length>0){for(let B of this.cleanupTaskList__)try{B.call(this)}catch(G){this.logger_.error("destroy","error_in_destroy_callback",G)}this.cleanupTaskList__.length=0}this.element_?.remove(),this.element_=null}autoDestroy(){if(this.logger_.logMethod?.("autoDestroy"),this.element_?.isConnected===!1)return this.destroy(),!0;return!1}}function z(B,G=!0,U){return function(X,P){if(P.kind!=="accessor")throw Error('@query can only be used with the "accessor" keyword');let Q=Symbol(`${String(P.name)}__`);return{get(){let J=this[Q];if(J===void 0||G===!1){let q=U??this.element_;J=this[Q]=q.querySelector(B)}return J}}}}function p(B,G=!0,U){return function(X,P){if(P.kind!=="accessor")throw Error('@queryAll can only be used with the "accessor" keyword');let Q=Symbol(`${String(P.name)}__`);return{get(){let J=this[Q];if(J===void 0||G===!1){let q=U??this.element_;J=this[Q]=q.querySelectorAll(B)}return J}}}}export{p as queryAll,z as query,k as directive,W as bootstrapDirectives,O as autoDestructDirectives,K as DirectiveBase};
3
3
 
4
- //# debugId=D842B0DE5E6144C864756E2164756E21
4
+ //# debugId=9F82674D4F3F0FD364756E2164756E21
5
5
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/lib.ts", "../src/bootstrap.ts", "../src/directive-decorator.ts", "../src/directive-class.ts", "../src/query-decorator.ts"],
3
+ "sources": ["../src/lib.ts", "../src/bootstrap.ts", "../src/auto-destruct.ts", "../src/directive-decorator.ts", "../src/directive-class.ts", "../src/query-decorator.ts"],
4
4
  "sourcesContent": [
5
- "import {createLogger} from '@alwatr/logger';\n\nimport type {DirectiveConstructor} from './directive-decorator.js';\n\n/**\n * Alwatr Synapse Logger.\n */\nexport const logger = createLogger('alwatr/synapse');\n\n/**\n * The registry for all directives.\n */\nexport const directiveRegistry_: {selector: string; constructor: DirectiveConstructor}[] = [];\n",
6
- "import {directiveRegistry_, logger} from './lib.js';\n\nconst initializedAttribute = 'synapse';\n\n/**\n * Initializes all registered directives within a given root element.\n * If no root element is provided, it scans the entire body.\n *\n * This function is idempotent; it will not re-initialize a directive on an element\n * that has already been processed.\n *\n * @param rootElement The element to scan for directives. Defaults to `document.body`.\n *\n * @example\n * ```ts\n * // Initialize directives on the whole page after the DOM is loaded.\n * document.addEventListener('DOMContentLoaded', () => bootstrapDirectives());\n *\n * // Or, initialize directives on a dynamically added part of the page.\n * const newContent = document.createElement('div');\n * newContent.innerHTML = '<div class=\"my-button\">Click Me</div>';\n * document.body.appendChild(newContent);\n *\n * bootstrapDirectives(newContent);\n * ```\n */\nexport function bootstrapDirectives(rootElement: Element | Document = document.body): void {\n logger.logMethod?.('bootstrapDirectives');\n\n if (document.readyState === 'loading') {\n logger.incident?.('bootstrapDirectives', 'dom_not_ready', 'Delaying directive initialization until DOM is ready');\n document.addEventListener('DOMContentLoaded', () => {\n bootstrapDirectives(rootElement);\n }, {once: true});\n return;\n }\n\n for (const {selector, constructor} of directiveRegistry_) {\n try {\n const uninitializedSelector = `${selector}:not([${initializedAttribute}])`;\n const elements = rootElement.querySelectorAll<HTMLElement>(uninitializedSelector);\n if (elements.length === 0) continue;\n\n logger.logOther?.(`Found ${elements.length} new element(s) for directive \"${selector}\"`);\n elements.forEach((element) => {\n // Mark the element as processed before creating an instance\n element.setAttribute(initializedAttribute, '');\n // Instantiate the directive with the element.\n new constructor(element, selector);\n });\n }\n catch (err) {\n logger.error('bootstrapDirectives', 'directive_instantiation_error', err, {selector});\n }\n }\n}\n",
7
- "import { directiveRegistry_, logger } from './lib.js';\n\nimport type { DirectiveBase } from './directive-class.js';\n\n/**\n * Type definition for a directive constructor.\n * A directive class must have a constructor that accepts an HTMLElement.\n */\nexport type DirectiveConstructor<T extends DirectiveBase = DirectiveBase> = new (element: HTMLElement, selector: string) => T;\n\n/**\n * A class decorator that registers a class as a directive.\n *\n * @param selector The CSS selector to which this directive will be attached.\n *\n * @example\n * ```ts\n * @directive('.my-button')\n * class MyButtonDirective extends DirectiveBase {\n * protected update_(): void {\n * this.element_.addEventListener('click', () => console.log('Button clicked!'));\n * }\n * }\n * ```\n */\nexport function directive(selector: string) {\n logger.logMethodArgs?.('@directive', selector);\n\n /**\n * The decorator function that receives the class constructor.\n * @param constructor The class to be registered as a directive.\n * @param context The decorator context.\n */\n return function (constructor: DirectiveConstructor, context: ClassDecoratorContext): void {\n if (context.kind !== 'class') {\n throw new Error('@directive can only be used on classes');\n }\n\n directiveRegistry_.push({\n selector,\n constructor\n });\n };\n}\n",
8
- "/**\n * @package @alwatr/synapse\n *\n * This file defines the `DirectiveBase` class, which is the foundation for creating custom directives\n * in the Alwatr Synapse library. Directives are used to attach behavior and logic to DOM elements\n * declaratively.\n */\n\nimport {delay} from '@alwatr/delay';\nimport {createLogger} from '@alwatr/logger';\n\n/**\n * The abstract base class for all directives.\n *\n * Extend this class to create a new directive that can be registered with the `@directive` decorator.\n * It provides the core functionality for linking a TypeScript class to a DOM element and managing its lifecycle.\n *\n * @example\n * ```ts\n * import {DirectiveBase, directive} from '@alwatr/synapse';\n *\n * @directive('[my-directive]')\n * export class MyDirective extends DirectiveBase {\n * protected override init_(): void {\n * super.init_(); // فراخوانی متد والد برای حفظ سازگاری با نسخه‌های قبل ضروری است\n * this.element_.textContent = 'Hello from MyDirective!';\n * this.element_.addEventListener('click', () => this.log('Element clicked!'));\n * }\n * }\n * ```\n */\nexport abstract class DirectiveBase {\n /**\n * The CSS selector that this directive is associated with.\n * This is the selector string provided to the `@directive` decorator.\n */\n protected readonly selector_;\n\n /**\n * A dedicated logger instance for this directive, pre-configured with a context like `directive:[selector]`.\n * Use this for logging to provide clear, contextual messages.\n */\n protected readonly logger_;\n\n /**\n * The DOM element to which this directive instance is attached.\n * All directive logic operates on this element.\n */\n protected readonly element_: HTMLElement;\n\n /**\n * A list of callback functions to be executed when the directive is destroyed.\n */\n private readonly cleanupTaskList__: NoopFunc[] = [];\n\n /**\n * Initializes the directive. This constructor is called by the Synapse bootstrap process and should not be\n * overridden in subclasses.\n *\n * It sets up the logger, element, and selector, and then schedules the `init_` and `update_` lifecycle methods\n * to run in the next microtask.\n *\n * @param element The DOM element to which this directive is attached.\n * @param selector The CSS selector that matched this directive.\n */\n constructor(element: HTMLElement, selector: string) {\n this.logger_ = createLogger(`directive:${selector}`);\n this.logger_.logMethodArgs?.('new', {selector, element});\n\n this.selector_ = selector;\n this.element_ = element;\n\n (async () => {\n await delay.nextMicrotask();\n await this.init_();\n })();\n }\n\n /**\n * Called once automatically after the directive is initialized.\n *\n * This method serves as the main entry point for your directive's logic,\n * such as modifying the element or setting up event listeners.\n *\n * **Note:** Do not call this method directly. It is designed to be called only once by the framework.\n */\n protected init_(): Awaitable<void> {\n this.logger_.logMethod?.('init_');\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).update_?.(); // backward compatibility\n }\n\n /**\n * Dispatches a custom event from the target element.\n *\n * This is a convenience method for firing events that can be listened to by other parts of the application.\n * The event bubbles up through the DOM.\n *\n * @param eventName The name of the custom event.\n * @param detail Optional data to include in the event's `detail` property.\n *\n * @example\n * ```ts\n * this.dispatch_('user-action', {action: 'save', id: 123});\n * ```\n */\n protected dispatch_(eventName: string, detail?: unknown): void {\n this.logger_.logMethodArgs?.('dispatch_', {eventName, detail});\n this.element_.dispatchEvent(new CustomEvent(eventName, {detail, bubbles: true}));\n }\n\n /**\n * Registers a task to be executed when the directive is destroyed.\n * Follows the `on[Event]` pattern, similar to `onClick`.\n * Useful for cleaning up resources, such as unsubscribing from signals or removing global event listeners.\n *\n * @param task The cleanup task to register.\n *\n * @example\n * ```ts\n * this.onDestroy(\n * signal.subscribe(() => this.log('signal changed')).unsubscribe\n * );\n * ```\n */\n protected onDestroy_(task: NoopFunc): void {\n this.logger_.logMethod?.('onDestroy_');\n this.cleanupTaskList__.push(task);\n }\n\n /**\n * Cleans up the directive's resources.\n *\n * This method removes the element from the DOM and nullifies the internal reference to it,\n * helping with garbage collection. It can be extended by subclasses to perform additional cleanup,\n * such as removing event listeners.\n */\n protected destroy_(): Awaitable<void> {\n this.logger_.logMethod?.('destroy_');\n\n // Execute all registered cleanup tasks\n if (this.cleanupTaskList__.length > 0) {\n for (const task of this.cleanupTaskList__) {\n try {\n task.call(this);\n }\n catch (err) {\n this.logger_.error('destroy_', 'error_in_destroy_callback', err);\n }\n }\n\n this.cleanupTaskList__.length = 0; // clear the list after executing all tasks\n }\n\n this.element_?.remove();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).element_ = null;\n }\n}\n",
5
+ "import {createLogger} from '@alwatr/logger';\n\nimport type {DirectiveConstructor} from './directive-decorator.js';\nimport type {DirectiveBase} from './directive-class.js';\n\n/**\n * Alwatr Synapse Logger.\n */\nexport const logger = createLogger('alwatr/synapse');\n\n/**\n * A Map to store registered directives.\n * The key is the directive selector (e.g., '.my-button'), and the value is the constructor class for that directive.\n * This registry is populated by the `@Directive` decorator when directives are defined.\n * The `bootstrapDirectives` function uses this registry to find and initialize directives on the page.\n */\nexport const directiveRegistry_ = new Map<string, DirectiveConstructor>();\n\n/**\n * A WeakMap to track which directives have been initialized on which elements.\n * The key is the DOM element, and the value is a Set of directive selectors that have been initialized on that element.\n * This allows us to prevent multiple initializations of the same directive on the same element, and also to clean up the tracking when elements are destroyed.\n * The `bootstrapDirectives` function updates this WeakMap as it initializes directives, and checks it to avoid re-initialization.\n */\nexport const initializedDirectives_ = new WeakMap<Element, Set<string>>();\n\nexport const directiveInstanceRegistry_ = new Set<DirectiveBase>();\n\nexport const finalizationRegistry =\n typeof FinalizationRegistry !== 'undefined' ?\n new FinalizationRegistry((heldValue) => {\n logger.logOther?.(`Directive ${heldValue} has been garbage collected successfully.`);\n })\n : null;\n",
6
+ "import {directiveInstanceRegistry_, directiveRegistry_, initializedDirectives_, logger} from './lib.js';\nimport type {DirectiveBase} from './directive-class.js';\n\n/**\n * Initializes all registered directives within a given root element.\n * If no root element is provided, it scans the entire body.\n *\n * This function is idempotent; it will not re-initialize a directive on an element\n * that has already been processed.\n *\n * @param rootElement The element to scan for directives. Defaults to `document.body`.\n *\n * @example\n * ```ts\n * // Initialize directives on the whole page after the DOM is loaded.\n * document.addEventListener('DOMContentLoaded', () => bootstrapDirectives());\n *\n * // Or, initialize directives on a dynamically added part of the page.\n * const newContent = document.createElement('div');\n * newContent.innerHTML = '<div class=\"my-button\">Click Me</div>';\n * document.body.appendChild(newContent);\n *\n * bootstrapDirectives(newContent);\n * ```\n */\nexport function bootstrapDirectives(rootElement: Element | Document = document.body): void {\n logger.logMethod?.('bootstrapDirectives');\n\n if (document.readyState === 'loading') {\n logger.incident?.('bootstrapDirectives', 'dom_not_ready', 'Delaying directive initialization until DOM is ready');\n document.addEventListener(\n 'DOMContentLoaded',\n () => {\n bootstrapDirectives(rootElement);\n },\n {once: true},\n );\n return;\n }\n\n for (const [selector, constructor] of directiveRegistry_) {\n try {\n const elementList = rootElement.querySelectorAll<HTMLElement>(selector);\n if (elementList.length === 0) {\n logger.logOther?.('no_elements_found', {selector});\n continue;\n }\n\n for (const element of elementList) {\n let alreadyInitializedSelector = initializedDirectives_.get(element);\n\n if (alreadyInitializedSelector?.has(selector)) {\n logger.incident?.('bootstrapDirectives', 'directive_already_initialized', {selector, element});\n continue;\n }\n\n if (!alreadyInitializedSelector) {\n alreadyInitializedSelector = new Set([selector]);\n initializedDirectives_.set(element, alreadyInitializedSelector);\n }\n\n try {\n const directiveInstance = new constructor(element, selector);\n alreadyInitializedSelector.add(selector);\n directiveInstance.onDestroy(cleanOnDestroy);\n directiveInstanceRegistry_.add(directiveInstance);\n } catch (err) {\n logger.error('bootstrapDirectives', 'directive_instantiation_error', {selector, element}, err);\n }\n }\n } catch (err) {\n logger.error('bootstrapDirectives', 'directive_instantiation_error', err, {selector});\n }\n }\n}\n\n/**\n * Cleans up the directive instance when it is destroyed.\n * @param this The directive instance to clean up.\n */\nfunction cleanOnDestroy(this: DirectiveBase) {\n const alreadyInitializedSelector = initializedDirectives_.get(this.element_);\n if (alreadyInitializedSelector) {\n alreadyInitializedSelector.delete(this.selector_);\n if (alreadyInitializedSelector.size === 0) {\n initializedDirectives_.delete(this.element_);\n }\n }\n}\n",
7
+ "import {directiveInstanceRegistry_, logger} from './lib';\n\nexport function autoDestructDirectives(): void {\n logger.logMethod?.('autoDestructDirectives');\n for (const directiveInstance of directiveInstanceRegistry_) {\n if (directiveInstance.autoDestroy()) {\n directiveInstanceRegistry_.delete(directiveInstance);\n }\n }\n}\n",
8
+ "import {directiveRegistry_, logger} from './lib.js';\n\nimport type {DirectiveBase} from './directive-class.js';\n\n/**\n * Type definition for a directive constructor.\n * A directive class must have a constructor that accepts an HTMLElement.\n */\nexport type DirectiveConstructor<T extends DirectiveBase = DirectiveBase> = new (\n element: HTMLElement,\n selector: string,\n) => T;\n\n/**\n * A class decorator that registers a class as a directive.\n *\n * @param selector The CSS selector to which this directive will be attached.\n *\n * @example\n * ```ts\n * @directive('.my-button')\n * class MyButtonDirective extends DirectiveBase {\n * protected update_(): void {\n * this.element_.addEventListener('click', () => console.log('Button clicked!'));\n * }\n * }\n * ```\n */\nexport function directive(selector: string) {\n logger.logMethodArgs?.('@directive', selector);\n\n /**\n * The decorator function that receives the class constructor.\n * @param constructor The class to be registered as a directive.\n * @param context The decorator context.\n */\n return function (constructor: DirectiveConstructor, context: ClassDecoratorContext): void {\n if (context.kind !== 'class') {\n throw new Error('@directive can only be used on classes');\n }\n\n if (directiveRegistry_.has(selector)) {\n logger.accident('@directive', 'duplicate_selector_registration', {selector});\n return;\n }\n\n directiveRegistry_.set(selector, constructor);\n };\n}\n",
9
+ "/**\n * @package @alwatr/synapse\n *\n * This file defines the `DirectiveBase` class, which is the foundation for creating custom directives\n * in the Alwatr Synapse library. Directives are used to attach behavior and logic to DOM elements\n * declaratively.\n */\n\nimport {delay} from '@alwatr/delay';\nimport {createLogger} from '@alwatr/logger';\nimport {finalizationRegistry} from './lib';\n\n/**\n * A Map to keep track of how many instances of each directive selector have been created. This is used to generate unique indices for directives that share the same selector.\n */\nconst selectorCount = new Map<string, number>();\n/**\n * Generates a unique index for a given directive selector. This is used to differentiate multiple instances of the same directive on the page.\n */\nfunction generateIndexForSelector(selector: string): number {\n const currentIndex = selectorCount.get(selector) ?? 0;\n selectorCount.set(selector, currentIndex + 1);\n return currentIndex;\n}\n\n/**\n * The abstract base class for all directives.\n *\n * Extend this class to create a new directive that can be registered with the `@directive` decorator.\n * It provides the core functionality for linking a TypeScript class to a DOM element and managing its lifecycle.\n *\n * @example\n * ```ts\n * import {DirectiveBase, directive} from '@alwatr/synapse';\n *\n * @directive('[my-directive]')\n * export class MyDirective extends DirectiveBase {\n * protected override init_(): void {\n * super.init_(); // فراخوانی متد والد برای حفظ سازگاری با نسخه‌های قبل ضروری است\n * this.element_.textContent = 'Hello from MyDirective!';\n * this.element_.addEventListener('click', () => this.log('Element clicked!'));\n * }\n * }\n * ```\n */\nexport abstract class DirectiveBase {\n /**\n * The CSS selector that this directive is associated with.\n * This is the selector string provided to the `@directive` decorator.\n */\n protected readonly selector_;\n\n /**\n * A dedicated logger instance for this directive, pre-configured with a context like `directive:[selector]`.\n * Use this for logging to provide clear, contextual messages.\n */\n protected readonly logger_;\n\n /**\n * The DOM element to which this directive instance is attached.\n * All directive logic operates on this element.\n */\n protected readonly element_: HTMLElement;\n\n /**\n * A list of callback functions to be executed when the directive is destroyed.\n */\n private readonly cleanupTaskList__: NoopFunc[] = [];\n\n public readonly index: number;\n\n /**\n * Initializes the directive. This constructor is called by the Synapse bootstrap process and should not be\n * overridden in subclasses.\n *\n * It sets up the logger, element, and selector, and then schedules the `init_` and `update_` lifecycle methods\n * to run in the next microtask.\n *\n * @param element The DOM element to which this directive is attached.\n * @param selector The CSS selector that matched this directive.\n */\n constructor(element: HTMLElement, selector: string) {\n this.index = generateIndexForSelector(selector);\n\n const identifier = `directive:${selector}/${this.index}`;\n this.logger_ = createLogger(identifier);\n this.logger_.logMethodArgs?.('new', {selector, element});\n\n this.selector_ = selector;\n this.element_ = element;\n finalizationRegistry?.register(this, identifier);\n\n (async () => {\n await delay.nextMicrotask();\n await this.init_();\n })();\n }\n\n /**\n * Called once automatically after the directive is initialized.\n *\n * This method serves as the main entry point for your directive's logic,\n * such as modifying the element or setting up event listeners.\n *\n * **Note:** Do not call this method directly. It is designed to be called only once by the framework.\n */\n protected init_(): Awaitable<void> {\n this.logger_.logMethod?.('init_');\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).update_?.(); // backward compatibility\n }\n\n /**\n * Dispatches a custom event from the target element.\n *\n * This is a convenience method for firing events that can be listened to by other parts of the application.\n * The event bubbles up through the DOM.\n *\n * @param eventName The name of the custom event.\n * @param detail Optional data to include in the event's `detail` property.\n *\n * @example\n * ```ts\n * this.dispatch('user-action', {action: 'save', id: 123});\n * ```\n */\n public dispatch(eventName: string, detail?: unknown): void {\n this.logger_.logMethodArgs?.('dispatch', {eventName, detail});\n this.element_.dispatchEvent(new CustomEvent(eventName, {detail, bubbles: true}));\n }\n\n /**\n * Registers a task to be executed when the directive is destroyed.\n * Follows the `on[Event]` pattern, similar to `onClick`.\n * Useful for cleaning up resources, such as unsubscribing from signals or removing global event listeners.\n *\n * @param task The cleanup task to register.\n *\n * @example\n * ```ts\n * this.onDestroy(\n * signal.subscribe(() => this.log('signal changed')).unsubscribe\n * );\n * ```\n */\n public onDestroy(task: (this: this) => Awaitable<void>): void {\n this.logger_.logMethod?.('onDestroy');\n this.cleanupTaskList__.push(task);\n }\n\n /**\n * Cleans up the directive's resources.\n *\n * This method removes the element from the DOM and nullifies the internal reference to it,\n * helping with garbage collection. It can be extended by subclasses to perform additional cleanup,\n * such as removing event listeners.\n */\n public destroy(): Awaitable<void> {\n this.logger_.logMethod?.('destroy');\n\n // Execute all registered cleanup tasks\n if (this.cleanupTaskList__.length > 0) {\n for (const task of this.cleanupTaskList__) {\n try {\n task.call(this);\n } catch (err) {\n this.logger_.error('destroy', 'error_in_destroy_callback', err);\n }\n }\n\n this.cleanupTaskList__.length = 0; // clear the list after executing all tasks\n }\n\n this.element_?.remove();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).element_ = null;\n }\n\n /**\n * Automatically destroys the directive if its associated element is no longer connected to the DOM.\n *\n * This method can be called periodically (e.g., in a `MutationObserver` or a cleanup loop) to ensure that\n * directives are properly cleaned up when their elements are removed from the DOM.\n *\n * **Note:** This method does not automatically run; you must call it as needed to check for disconnected elements.\n */\n public autoDestroy(): boolean {\n this.logger_.logMethod?.('autoDestroy');\n if (this.element_?.isConnected === false) {\n void this.destroy();\n return true;\n }\n return false;\n }\n}\n",
9
10
  "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type { DirectiveBase } from './directive-class.js';\n\n/**\n * A property decorator that queries the directive's element for a selector.\n * The query is performed once and the result is cached.\n *\n * @param selector The CSS selector to query for.\n * @param cache Whether to cache the result on first access. Defaults is true.\n * @param root Optional root element to perform the query on. Defaults to the directive's element.\n *\n * @example\n * ```ts\n * @directive('[my-directive]')\n * class MyDirective extends DirectiveBase {\n * @query('.my-element')\n * protected myElement!: HTMLDivElement | null;\n * }\n * ```\n */\nexport function query<T extends Element>(selector: string, cache = true, root?: ParentNode) {\n /**\n * The decorator function that receives the property accessor.\n * @param target The prototype of the class.\n * @param context The decorator context.\n * @return A property descriptor with a getter that performs the query and caches the result.\n */\n return function (\n _target: ClassAccessorDecoratorTarget<DirectiveBase, T | null>,\n context: ClassAccessorDecoratorContext<DirectiveBase, T | null>\n ): ClassAccessorDecoratorResult<DirectiveBase, T | null> {\n if (context.kind !== 'accessor') {\n throw new Error('@query can only be used with the \"accessor\" keyword');\n }\n\n const privateKey = Symbol(`${String(context.name)}__`);\n\n return {\n get() {\n let value = (this as any)[privateKey] as T | null | undefined;\n if (value === undefined || cache === false) {\n const parent = root ?? this.element_;\n value = (this as any)[privateKey] = parent.querySelector<T>(selector);\n }\n return value;\n }\n };\n };\n}\n\n/**\n * A property decorator that queries the directive's element for all selectors.\n * The queries are performed once and the result is cached.\n *\n * @param selector The CSS selector to query for.\n * @param cache Whether to cache the result on first access. Defaults is true.\n * @param root Optional root element to perform the query on. Defaults to the directive's element.\n *\n * @example\n * ```ts\n * @directive('[my-directive]')\n * class MyDirective extends DirectiveBase {\n * @queryAll('.my-elements')\n * protected myElements!: NodeListOf<HTMLDivElement>;\n * }\n * ```\n */\nexport function queryAll<T extends Element>(selector: string, cache = true, root?: ParentNode) {\n /**\n * The decorator function that receives the property accessor.\n * @param target The prototype of the class.\n * @param context The decorator context.\n * @return A property descriptor with a getter that performs the query and caches the result.\n */\n return function (\n _target: ClassAccessorDecoratorTarget<DirectiveBase, NodeListOf<T>>,\n context: ClassAccessorDecoratorContext<DirectiveBase, NodeListOf<T>>\n ): ClassAccessorDecoratorResult<DirectiveBase, NodeListOf<T>> {\n if (context.kind !== 'accessor') {\n throw new Error('@queryAll can only be used with the \"accessor\" keyword');\n }\n\n const privateKey = Symbol(`${String(context.name)}__`);\n\n return {\n get() {\n let value = (this as any)[privateKey] as NodeListOf<T> | undefined;\n if (value === undefined || cache === false) {\n const parent = root ?? this.element_;\n value = (this as any)[privateKey] = parent.querySelectorAll<T>(selector);\n }\n return value;\n }\n };\n };\n}\n"
10
11
  ],
11
- "mappings": "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",
12
- "debugId": "D842B0DE5E6144C864756E2164756E21",
12
+ "mappings": "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",
13
+ "debugId": "9F82674D4F3F0FD364756E2164756E21",
13
14
  "names": []
14
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alwatr/synapse",
3
- "version": "9.3.0",
3
+ "version": "9.4.0",
4
4
  "description": "Connect your TypeScript classes to the DOM, declaratively.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
@@ -21,12 +21,12 @@
21
21
  },
22
22
  "sideEffects": false,
23
23
  "dependencies": {
24
- "@alwatr/delay": "9.3.0",
25
- "@alwatr/logger": "9.3.0"
24
+ "@alwatr/delay": "9.4.0",
25
+ "@alwatr/logger": "9.4.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@alwatr/nano-build": "9.3.0",
29
- "@alwatr/standard": "9.3.0",
29
+ "@alwatr/standard": "9.4.0",
30
30
  "@alwatr/type-helper": "9.1.1",
31
31
  "@happy-dom/global-registrator": "^20.8.9",
32
32
  "typescript": "^6.0.2"
@@ -79,5 +79,5 @@
79
79
  "web-components",
80
80
  "web-development"
81
81
  ],
82
- "gitHead": "adf6d486667eefee42dfc87938e3af96f87c4738"
82
+ "gitHead": "662f1047fc2a1b53a00902028d6cebfd04b52dd5"
83
83
  }
@@ -0,0 +1,10 @@
1
+ import {directiveInstanceRegistry_, logger} from './lib';
2
+
3
+ export function autoDestructDirectives(): void {
4
+ logger.logMethod?.('autoDestructDirectives');
5
+ for (const directiveInstance of directiveInstanceRegistry_) {
6
+ if (directiveInstance.autoDestroy()) {
7
+ directiveInstanceRegistry_.delete(directiveInstance);
8
+ }
9
+ }
10
+ }
package/src/bootstrap.ts CHANGED
@@ -1,6 +1,5 @@
1
- import {directiveRegistry_, logger} from './lib.js';
2
-
3
- const initializedAttribute = 'synapse';
1
+ import {directiveInstanceRegistry_, directiveRegistry_, initializedDirectives_, logger} from './lib.js';
2
+ import type {DirectiveBase} from './directive-class.js';
4
3
 
5
4
  /**
6
5
  * Initializes all registered directives within a given root element.
@@ -29,28 +28,62 @@ export function bootstrapDirectives(rootElement: Element | Document = document.b
29
28
 
30
29
  if (document.readyState === 'loading') {
31
30
  logger.incident?.('bootstrapDirectives', 'dom_not_ready', 'Delaying directive initialization until DOM is ready');
32
- document.addEventListener('DOMContentLoaded', () => {
33
- bootstrapDirectives(rootElement);
34
- }, {once: true});
31
+ document.addEventListener(
32
+ 'DOMContentLoaded',
33
+ () => {
34
+ bootstrapDirectives(rootElement);
35
+ },
36
+ {once: true},
37
+ );
35
38
  return;
36
39
  }
37
40
 
38
- for (const {selector, constructor} of directiveRegistry_) {
41
+ for (const [selector, constructor] of directiveRegistry_) {
39
42
  try {
40
- const uninitializedSelector = `${selector}:not([${initializedAttribute}])`;
41
- const elements = rootElement.querySelectorAll<HTMLElement>(uninitializedSelector);
42
- if (elements.length === 0) continue;
43
-
44
- logger.logOther?.(`Found ${elements.length} new element(s) for directive "${selector}"`);
45
- elements.forEach((element) => {
46
- // Mark the element as processed before creating an instance
47
- element.setAttribute(initializedAttribute, '');
48
- // Instantiate the directive with the element.
49
- new constructor(element, selector);
50
- });
51
- }
52
- catch (err) {
43
+ const elementList = rootElement.querySelectorAll<HTMLElement>(selector);
44
+ if (elementList.length === 0) {
45
+ logger.logOther?.('no_elements_found', {selector});
46
+ continue;
47
+ }
48
+
49
+ for (const element of elementList) {
50
+ let alreadyInitializedSelector = initializedDirectives_.get(element);
51
+
52
+ if (alreadyInitializedSelector?.has(selector)) {
53
+ logger.incident?.('bootstrapDirectives', 'directive_already_initialized', {selector, element});
54
+ continue;
55
+ }
56
+
57
+ if (!alreadyInitializedSelector) {
58
+ alreadyInitializedSelector = new Set([selector]);
59
+ initializedDirectives_.set(element, alreadyInitializedSelector);
60
+ }
61
+
62
+ try {
63
+ const directiveInstance = new constructor(element, selector);
64
+ alreadyInitializedSelector.add(selector);
65
+ directiveInstance.onDestroy(cleanOnDestroy);
66
+ directiveInstanceRegistry_.add(directiveInstance);
67
+ } catch (err) {
68
+ logger.error('bootstrapDirectives', 'directive_instantiation_error', {selector, element}, err);
69
+ }
70
+ }
71
+ } catch (err) {
53
72
  logger.error('bootstrapDirectives', 'directive_instantiation_error', err, {selector});
54
73
  }
55
74
  }
56
75
  }
76
+
77
+ /**
78
+ * Cleans up the directive instance when it is destroyed.
79
+ * @param this The directive instance to clean up.
80
+ */
81
+ function cleanOnDestroy(this: DirectiveBase) {
82
+ const alreadyInitializedSelector = initializedDirectives_.get(this.element_);
83
+ if (alreadyInitializedSelector) {
84
+ alreadyInitializedSelector.delete(this.selector_);
85
+ if (alreadyInitializedSelector.size === 0) {
86
+ initializedDirectives_.delete(this.element_);
87
+ }
88
+ }
89
+ }
@@ -8,6 +8,20 @@
8
8
 
9
9
  import {delay} from '@alwatr/delay';
10
10
  import {createLogger} from '@alwatr/logger';
11
+ import {finalizationRegistry} from './lib';
12
+
13
+ /**
14
+ * A Map to keep track of how many instances of each directive selector have been created. This is used to generate unique indices for directives that share the same selector.
15
+ */
16
+ const selectorCount = new Map<string, number>();
17
+ /**
18
+ * Generates a unique index for a given directive selector. This is used to differentiate multiple instances of the same directive on the page.
19
+ */
20
+ function generateIndexForSelector(selector: string): number {
21
+ const currentIndex = selectorCount.get(selector) ?? 0;
22
+ selectorCount.set(selector, currentIndex + 1);
23
+ return currentIndex;
24
+ }
11
25
 
12
26
  /**
13
27
  * The abstract base class for all directives.
@@ -53,6 +67,8 @@ export abstract class DirectiveBase {
53
67
  */
54
68
  private readonly cleanupTaskList__: NoopFunc[] = [];
55
69
 
70
+ public readonly index: number;
71
+
56
72
  /**
57
73
  * Initializes the directive. This constructor is called by the Synapse bootstrap process and should not be
58
74
  * overridden in subclasses.
@@ -64,11 +80,15 @@ export abstract class DirectiveBase {
64
80
  * @param selector The CSS selector that matched this directive.
65
81
  */
66
82
  constructor(element: HTMLElement, selector: string) {
67
- this.logger_ = createLogger(`directive:${selector}`);
83
+ this.index = generateIndexForSelector(selector);
84
+
85
+ const identifier = `directive:${selector}/${this.index}`;
86
+ this.logger_ = createLogger(identifier);
68
87
  this.logger_.logMethodArgs?.('new', {selector, element});
69
88
 
70
89
  this.selector_ = selector;
71
90
  this.element_ = element;
91
+ finalizationRegistry?.register(this, identifier);
72
92
 
73
93
  (async () => {
74
94
  await delay.nextMicrotask();
@@ -102,11 +122,11 @@ export abstract class DirectiveBase {
102
122
  *
103
123
  * @example
104
124
  * ```ts
105
- * this.dispatch_('user-action', {action: 'save', id: 123});
125
+ * this.dispatch('user-action', {action: 'save', id: 123});
106
126
  * ```
107
127
  */
108
- protected dispatch_(eventName: string, detail?: unknown): void {
109
- this.logger_.logMethodArgs?.('dispatch_', {eventName, detail});
128
+ public dispatch(eventName: string, detail?: unknown): void {
129
+ this.logger_.logMethodArgs?.('dispatch', {eventName, detail});
110
130
  this.element_.dispatchEvent(new CustomEvent(eventName, {detail, bubbles: true}));
111
131
  }
112
132
 
@@ -124,8 +144,8 @@ export abstract class DirectiveBase {
124
144
  * );
125
145
  * ```
126
146
  */
127
- protected onDestroy_(task: NoopFunc): void {
128
- this.logger_.logMethod?.('onDestroy_');
147
+ public onDestroy(task: (this: this) => Awaitable<void>): void {
148
+ this.logger_.logMethod?.('onDestroy');
129
149
  this.cleanupTaskList__.push(task);
130
150
  }
131
151
 
@@ -136,17 +156,16 @@ export abstract class DirectiveBase {
136
156
  * helping with garbage collection. It can be extended by subclasses to perform additional cleanup,
137
157
  * such as removing event listeners.
138
158
  */
139
- protected destroy_(): Awaitable<void> {
140
- this.logger_.logMethod?.('destroy_');
159
+ public destroy(): Awaitable<void> {
160
+ this.logger_.logMethod?.('destroy');
141
161
 
142
162
  // Execute all registered cleanup tasks
143
163
  if (this.cleanupTaskList__.length > 0) {
144
164
  for (const task of this.cleanupTaskList__) {
145
165
  try {
146
166
  task.call(this);
147
- }
148
- catch (err) {
149
- this.logger_.error('destroy_', 'error_in_destroy_callback', err);
167
+ } catch (err) {
168
+ this.logger_.error('destroy', 'error_in_destroy_callback', err);
150
169
  }
151
170
  }
152
171
 
@@ -157,4 +176,21 @@ export abstract class DirectiveBase {
157
176
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
158
177
  (this as any).element_ = null;
159
178
  }
179
+
180
+ /**
181
+ * Automatically destroys the directive if its associated element is no longer connected to the DOM.
182
+ *
183
+ * This method can be called periodically (e.g., in a `MutationObserver` or a cleanup loop) to ensure that
184
+ * directives are properly cleaned up when their elements are removed from the DOM.
185
+ *
186
+ * **Note:** This method does not automatically run; you must call it as needed to check for disconnected elements.
187
+ */
188
+ public autoDestroy(): boolean {
189
+ this.logger_.logMethod?.('autoDestroy');
190
+ if (this.element_?.isConnected === false) {
191
+ void this.destroy();
192
+ return true;
193
+ }
194
+ return false;
195
+ }
160
196
  }
@@ -1,12 +1,15 @@
1
- import { directiveRegistry_, logger } from './lib.js';
1
+ import {directiveRegistry_, logger} from './lib.js';
2
2
 
3
- import type { DirectiveBase } from './directive-class.js';
3
+ import type {DirectiveBase} from './directive-class.js';
4
4
 
5
5
  /**
6
6
  * Type definition for a directive constructor.
7
7
  * A directive class must have a constructor that accepts an HTMLElement.
8
8
  */
9
- export type DirectiveConstructor<T extends DirectiveBase = DirectiveBase> = new (element: HTMLElement, selector: string) => T;
9
+ export type DirectiveConstructor<T extends DirectiveBase = DirectiveBase> = new (
10
+ element: HTMLElement,
11
+ selector: string,
12
+ ) => T;
10
13
 
11
14
  /**
12
15
  * A class decorator that registers a class as a directive.
@@ -36,9 +39,11 @@ export function directive(selector: string) {
36
39
  throw new Error('@directive can only be used on classes');
37
40
  }
38
41
 
39
- directiveRegistry_.push({
40
- selector,
41
- constructor
42
- });
42
+ if (directiveRegistry_.has(selector)) {
43
+ logger.accident('@directive', 'duplicate_selector_registration', {selector});
44
+ return;
45
+ }
46
+
47
+ directiveRegistry_.set(selector, constructor);
43
48
  };
44
49
  }
package/src/lib.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {createLogger} from '@alwatr/logger';
2
2
 
3
3
  import type {DirectiveConstructor} from './directive-decorator.js';
4
+ import type {DirectiveBase} from './directive-class.js';
4
5
 
5
6
  /**
6
7
  * Alwatr Synapse Logger.
@@ -8,6 +9,26 @@ import type {DirectiveConstructor} from './directive-decorator.js';
8
9
  export const logger = createLogger('alwatr/synapse');
9
10
 
10
11
  /**
11
- * The registry for all directives.
12
+ * A Map to store registered directives.
13
+ * The key is the directive selector (e.g., '.my-button'), and the value is the constructor class for that directive.
14
+ * This registry is populated by the `@Directive` decorator when directives are defined.
15
+ * The `bootstrapDirectives` function uses this registry to find and initialize directives on the page.
12
16
  */
13
- export const directiveRegistry_: {selector: string; constructor: DirectiveConstructor}[] = [];
17
+ export const directiveRegistry_ = new Map<string, DirectiveConstructor>();
18
+
19
+ /**
20
+ * A WeakMap to track which directives have been initialized on which elements.
21
+ * The key is the DOM element, and the value is a Set of directive selectors that have been initialized on that element.
22
+ * This allows us to prevent multiple initializations of the same directive on the same element, and also to clean up the tracking when elements are destroyed.
23
+ * The `bootstrapDirectives` function updates this WeakMap as it initializes directives, and checks it to avoid re-initialization.
24
+ */
25
+ export const initializedDirectives_ = new WeakMap<Element, Set<string>>();
26
+
27
+ export const directiveInstanceRegistry_ = new Set<DirectiveBase>();
28
+
29
+ export const finalizationRegistry =
30
+ typeof FinalizationRegistry !== 'undefined' ?
31
+ new FinalizationRegistry((heldValue) => {
32
+ logger.logOther?.(`Directive ${heldValue} has been garbage collected successfully.`);
33
+ })
34
+ : null;
package/src/main.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './bootstrap.js';
2
+ export * from './auto-destruct.js';
2
3
  export * from './directive-decorator.js';
3
4
  export * from './directive-class.js';
4
5
  export * from './query-decorator.js';