/** * Main SDK class * * Composes all capabilities and provides the plugin registration system. * Follows functional plugin architecture with capability injection. */ import { Config } from './capabilities/config'; import { Emitter } from './capabilities/emitter'; import { Expose } from './capabilities/expose'; import { Namespace } from './capabilities/namespace'; import type { Plugin, PluginFunction, SDKConfig } from './types'; /** * SDK - Main SDK class for building JavaScript SDKs * * @example * ```typescript * const sdk = new SDK({ name: 'my-sdk' }); * * sdk.use((plugin, instance, config) => { * plugin.ns('my.plugin'); * plugin.defaults({ my: { setting: 'value' } }); * plugin.expose({ * track(event: string) { * console.log('Tracking:', event); * } * }); * }); * * await sdk.init(); * sdk.track('page_view'); * ``` */ export class SDK { /** * Configuration instance * @private */ private configInstance: Config; /** * Event emitter instance * @private */ private emitter: Emitter; /** * Registered plugins by namespace * @private */ private plugins: Map = new Map(); /** * Registry of capabilities held by plugins * @private */ private capabilities: Map = new Map(); /** * Initialization state * @private */ private isInitialized = false; /** * Create a new SDK instance. * * @param config - Initial SDK configuration * * @example * ```typescript * const sdk = new SDK({ * name: 'my-sdk', * api: { * endpoint: 'https://api.example.com', * timeout: 5000 * } * }); * ``` */ constructor(config: SDKConfig = {}) { this.emitter = new Emitter(); this.configInstance = new Config(config); } /** * Register a plugin. * * Plugins receive capability-injected objects and can extend the SDK. * Supports method chaining. * * @param pluginFn - Plugin function * @returns this - For method chaining * * @example * ```typescript * sdk * .use(analyticsPlugin) * .use(trackingPlugin) * .init(); * ``` */ use(pluginFn: PluginFunction): this { // Create capability instances for this plugin const namespace = new Namespace(); const expose = new Expose(this); // Compose Plugin object with all capabilities const plugin: Plugin = { // Namespace property (updated when ns() is called) namespace: undefined, // Namespace capability (wrapped to update plugin.namespace) ns: (ns: string) => { namespace.ns(ns); (plugin as any).namespace = namespace.name; }, // Config capabilities defaults: this.configInstance.defaults.bind(this.configInstance), // Emitter capabilities on: this.emitter.on.bind(this.emitter), off: this.emitter.off.bind(this.emitter), emit: this.emitter.emit.bind(this.emitter), // Expose capability expose: expose.expose.bind(expose), // Requirer capability mustEnable: () => { if (namespace.name) { this.configInstance.markRequired(namespace.name); } }, // Extensible capability hold: (methods: Record) => { for (const [name, fn] of Object.entries(methods)) { // Warn if capability already registered if (this.capabilities.has(name)) { console.warn( `[sdk-kit] Capability "${name}" already registered. ` + `Plugin "${namespace.name}" is overwriting it.` ); } // Store the unbound function so we can rebind it for each consumer this.capabilities.set(name, fn); } }, hasCapability: (name: string): boolean => { return this.capabilities.has(name); }, } as Plugin; // Copy all registered capabilities to this plugin instance // Rebind each capability so "this" refers to the consuming plugin for (const [name, fn] of this.capabilities.entries()) { (plugin as any)[name] = fn.bind(plugin); } // Execute plugin function with capability injection pluginFn(plugin, this, this.configInstance); // Store plugin by namespace if (namespace.name) { this.plugins.set(namespace.name, { namespace, plugin }); } return this; } /** * Initialize the SDK. * * Emits `sdk:init` event before initialization and `sdk:ready` after. * Idempotent - can be called multiple times safely. * * @param config - Optional configuration to merge with existing config * @returns Promise that resolves when initialization is complete * * @example * ```typescript * sdk.on('sdk:ready', () => { * console.log('SDK is ready!'); * }); * * await sdk.init({ api: { timeout: 5000 } }); * ``` */ async init(config?: SDKConfig): Promise { if (this.isInitialized) { console.warn('SDK already initialized'); return; } // Merge config if provided (new values override defaults) if (config) { this.configInstance.merge(config); } // Emit init event (plugins can listen for this) this.emitter.emit('sdk:init'); // Check for required plugins const requiredNamespaces = Array.from(this.plugins.keys()).filter((ns) => this.configInstance.isRequired(ns) ); for (const ns of requiredNamespaces) { if (!this.plugins.has(ns)) { throw new Error(`Required plugin "${ns}" is not registered`); } } this.isInitialized = true; // Emit ready event this.emitter.emit('sdk:ready'); } /** * Destroy the SDK. * * Emits `sdk:destroy` event, clears all plugins, and removes all event listeners. * * @returns Promise that resolves when destruction is complete * * @example * ```typescript * await sdk.destroy(); * ``` */ async destroy(): Promise { // Emit destroy event (plugins can listen for cleanup) this.emitter.emit('sdk:destroy'); // Clear plugins this.plugins.clear(); // Remove all event listeners this.emitter.removeAllListeners(); this.isInitialized = false; } /** * Get a configuration value by path. * * Delegates to the Config capability. * * @param path - Dot-notation path (e.g., 'api.timeout') * @returns Configuration value * * @example * ```typescript * const timeout = sdk.get('api.timeout'); * const nested = sdk.get('my.plugin.setting'); * ``` */ get(path: string): T { return this.configInstance.get(path); } /** * Set a configuration value by path. * * Delegates to the Config capability. * * @param path - Dot-notation path (e.g., 'api.timeout') * @param value - Value to set * * @example * ```typescript * sdk.set('api.timeout', 10000); * sdk.set('my.plugin.enabled', true); * ``` */ set(path: string, value: any): void { this.configInstance.set(path, value); } /** * Subscribe to an event. * * Delegates to the Emitter capability. Supports wildcard patterns. * * @param event - Event name or pattern (e.g., 'track:*') * @param handler - Event handler function * @returns Unsubscribe function * * @example * ```typescript * const unsubscribe = sdk.on('track:page', (data) => { * console.log('Page tracked:', data); * }); * * // Later... * unsubscribe(); * ``` */ on(event: string, handler: (...args: any[]) => void): () => void { return this.emitter.on(event, handler); } /** * Unsubscribe from an event. * * Delegates to the Emitter capability. * * @param event - Event name * @param handler - Event handler function to remove * * @example * ```typescript * const handler = () => console.log('ready'); * sdk.on('sdk:ready', handler); * sdk.off('sdk:ready', handler); * ``` */ off(event: string, handler: (...args: any[]) => void): void { this.emitter.off(event, handler); } /** * Emit an event. * * Delegates to the Emitter capability. * * @param event - Event name * @param args - Event arguments * * @example * ```typescript * sdk.emit('custom:event', { data: 'value' }); * ``` */ emit(event: string, ...args: any[]): void { this.emitter.emit(event, ...args); } /** * Get all configuration as an immutable copy. * * Delegates to the Config capability. * * @returns Copy of the entire configuration object * * @example * ```typescript * const allConfig = sdk.getAll(); * console.log(allConfig); * ``` */ getAll(): Record { return this.configInstance.getAll(); } /** * Check if SDK is initialized. * * @returns true if initialized, false otherwise * * @example * ```typescript * if (sdk.isReady()) { * sdk.track('event'); * } * ``` */ isReady(): boolean { return this.isInitialized; } }