{"_id":"@apihive/signals","name":"@apihive/signals","dist-tags":{"latest":"1.0.7"},"versions":{"1.0.7":{"name":"@apihive/signals","version":"1.0.7","author":{"name":"Nicola Dal Pont"},"license":"MIT","main":"./lib/index.js","types":"./lib/index.d.ts","repository":{"type":"git","url":"git+https://github.com/cleverplatypus/signals-ts.git"},"exports":{".":{"require":"./lib/index.js","types":"./lib/index.d.ts"},"./package.json":"./package.json"},"scripts":{"build":"tsc -p tsconfig.json","prepublishOnly":"yarn build","publish:npm":"npm publish --access public","test":"vitest"},"devDependencies":{"typescript":"^5.9.2","vitest":"^3.2.4"},"_id":"@apihive/signals@1.0.7","gitHead":"24de308fd0a00d1a0ce0d1211fdc5a740ccb8cfb","description":"<p align=\"center\">   <img src=\"./logo.png\" alt=\"APIHive Signals logo\" width=\"120\" /> </p>","bugs":{"url":"https://github.com/cleverplatypus/signals-ts/issues"},"homepage":"https://github.com/cleverplatypus/signals-ts#readme","_nodeVersion":"22.13.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-H/iRl39k3scPah00+QhtCuTTqiZb2fS6OpshDINbm8x8rYelOuZ7916xLRqB2/3TEx7Vn9ceisxvEEFaRFa8kQ==","shasum":"e2d54ee0ccd04feab67e72eda47a1f12071a2860","tarball":"https://registry.npmjs.org/@apihive/signals/-/signals-1.0.7.tgz","fileCount":12,"unpackedSize":31887,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDj7JKE/r/7Kz+pmJv+C9oO73xPkZ4wW8m+W7jWH5hrdQIhALNTToMSBmvTc7e6Q1oPx2X+0cSS5nn54L5Hnq59hufx"}]},"_npmUser":{"name":"aekidna","email":"ndalpont@gmail.com"},"directories":{},"maintainers":[{"name":"aekidna","email":"ndalpont@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/signals_1.0.7_1756882530691_0.3205820693467283"},"_hasShrinkwrap":false}},"time":{"created":"2025-09-03T06:55:30.572Z","1.0.7":"2025-09-03T06:55:30.866Z","modified":"2025-09-03T06:55:31.149Z"},"maintainers":[{"name":"aekidna","email":"ndalpont@gmail.com"}],"description":"<p align=\"center\">   <img src=\"./logo.png\" alt=\"APIHive Signals logo\" width=\"120\" /> </p>","homepage":"https://github.com/cleverplatypus/signals-ts#readme","repository":{"type":"git","url":"git+https://github.com/cleverplatypus/signals-ts.git"},"author":{"name":"Nicola Dal Pont"},"bugs":{"url":"https://github.com/cleverplatypus/signals-ts/issues"},"license":"MIT","readme":"<p align=\"center\">\n  <img src=\"./logo.png\" alt=\"APIHive Signals logo\" width=\"120\" />\n</p>\n\n<h1 align=\"center\">APIHive Signals</h1>\n<p align=\"center\"><i>TypeScript-first pub-sub messaging — decoupled, type-safe, and powerful.</i></p>\n\n<p align=\"center\">\n  <a href=\"https://www.npmjs.com/package/@apihive/signals\">\n    <img alt=\"npm version\" src=\"https://img.shields.io/npm/v/@apihive/signals?color=cb0000&label=npm&logo=npm\" />\n  </a>\n  <a href=\"https://www.npmjs.com/package/@apihive/signals\">\n    <img alt=\"npm downloads\" src=\"https://img.shields.io/npm/dm/@apihive/signals\" />\n  </a>\n  <a href=\"https://github.com/cleverplatypus/signals-ts/actions/workflows/test.yml\">\n    <img alt=\"Tests\" src=\"https://github.com/cleverplatypus/signals-ts/actions/workflows/test.yml/badge.svg\" />\n  </a>\n  <a href=\"https://vitest.dev/\">\n    <img alt=\"tested with vitest\" src=\"https://img.shields.io/badge/tested%20with-vitest-6E9F18?logo=vitest\" />\n  </a>\n  <a href=\"https://www.typescriptlang.org/\">\n    <img alt=\"TypeScript\" src=\"https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript\" />\n  </a>\n  <a href=\"LICENSE\">\n    <img alt=\"License: Apache 2.0\" src=\"https://img.shields.io/badge/License-Apache%202.0-green.svg\" />\n  </a>\n  <a href=\"https://bundlephobia.com/package/@apihive/signals\">\n    <img alt=\"bundle size\" src=\"https://img.shields.io/bundlephobia/minzip/@apihive/signals?label=minzipped\" />\n  </a>\n</p>\n\n**APIHive Signals** (formerly **Signals.ts**) is a modern, TypeScript-first pub-sub messaging system that enables completely decoupled communication between components. Built for applications that need reliable, type-safe message passing without tight coupling.\n\n> **Breaking change from v1.0.4 to v1.0.5**: Signals now resolve to a single value instead of an array when only one listener is attached.\n\n## Why Signals?\n\nEvent systems in web applications are nearly always embedded in the visual components, which forces the propagation and handling of events to be tied to the visual hierarchy. This works for many use cases but it fails to provide the flexibility needed when dealing with cross cutting concerns. They also have some overhead because the events need to travel up and down the visual hierarchy to reach their targets.\n\nFurthermore they lack asyncronous bidirectional communication capabilities, which is often needed in modern applications.\n\nSignals act as decoupled intermediaries between components, enabling bidirectional communication without tight coupling.\n\nInspired by [JS-Signals](https://github.com/millermedeiros/js-signals) and [AS3-Signals](https://github.com/robertpenner/as3-signals), but built from the ground up for modern TypeScript applications.\n\n## Feature Highlights\n\n- **Type-safe messaging**\n  - Generic payload types with full IntelliSense support\n  - Typed return values from listeners\n  - Compile-time safety for message contracts\n\n- **Advanced listener control**\n  - Priority-based execution order\n  - One-time listeners with `addOnce()`\n  - Suspend/resume individual listeners\n  - Binding management with cleanup\n\n- **Flexible propagation modes**\n  - `all` — Wait for all listeners (default)\n  - `any` — Stop after first successful response\n  - `any-fail` — Stop after first failure\n  - `none` — Expect all listeners to fail\n\n- **Async-first design**\n  - Promise-based dispatch with `await`\n  - Mixed sync/async listener support\n  - Sequential execution with proper error handling\n\n- **Memory and lifecycle management**\n  - Signal memoization for late subscribers\n  - Automatic cleanup and disposal\n  - Signal suspension for temporary disabling\n\n## Installation\n\nUsing npm:\n```bash\nnpm install @apihive/signals\n```\n\nUsing yarn:\n```bash\nyarn add @apihive/signals\n```\n\nUsing pnpm:\n```bash\npnpm add @apihive/signals\n```\n\n## Quick Start\n\n### Basic messaging\n```ts\nimport { Signal } from '@apihive/signals';\n\n// Create a signal\nconst userLoggedIn = new Signal<{ userId: string, username: string }>();\n\n// Add a listener\nuserLoggedIn.add((user) => {\n  console.log(`Welcome back, ${user.username}!`);\n});\n\n// Dispatch the signal\nuserLoggedIn.dispatch({ userId: '123', username: 'alice' });\n```\n\n### Request-response pattern\n```ts\nconst showConfirmDialog = new Signal<{ \n  message: string, \n  title?: string \n}, boolean>();\n\n// Listener returns a response\nshowConfirmDialog.add(async ({ message, title = 'Confirm' }) => {\n  return window.confirm(`${title}: ${message}`);\n});\n\n// Dispatcher awaits the response\nconst confirmed = await showConfirmDialog.dispatch({\n  message: 'Delete this item?',\n  title: 'Are you sure?'\n});\n\nif (confirmed) {\n  // Proceed with deletion\n}\n```\n\n### Application lifecycle with memoization\n```ts\n// Signal that remembers its last dispatch\nconst appInitialized = new Signal<void>({ memoize: true });\n\n// Bootstrap process\nasync function bootstrap() {\n  await loadConfig();\n  await connectDatabase();\n  appInitialized.dispatch(); // Signal initialization complete\n}\n\n// Components can listen even after initialization\nappInitialized.addOnce(() => {\n  // This will fire immediately if app is already initialized\n  startPeriodicTasks();\n});\n```\n\n## Real-World Examples\n\n### E-commerce Cart System\n```ts\n// Define your signals\nconst cartSignals = {\n  itemAdded: new Signal<{ productId: string, quantity: number }>(),\n  itemRemoved: new Signal<{ productId: string }>(),\n  cartCleared: new Signal<void>(),\n  checkoutStarted: new Signal<{ cartTotal: number }, boolean>()\n};\n\n// Cart component listens and updates UI\ncartSignals.itemAdded.add(({ productId, quantity }) => {\n  updateCartBadge();\n  showNotification(`Added ${quantity}x ${productId} to cart`);\n});\n\n// Inventory component listens and updates stock\ncartSignals.itemAdded.add(({ productId, quantity }) => {\n  updateInventoryCount(productId, -quantity);\n});\n\n// Analytics component tracks events\ncartSignals.itemAdded.add(({ productId, quantity }) => {\n  analytics.track('cart_item_added', { productId, quantity });\n});\n\n// Checkout validation\ncartSignals.checkoutStarted.add(async ({ cartTotal }) => {\n  const hasValidPayment = await validatePaymentMethod();\n  const hasStock = await validateInventory();\n  return hasValidPayment && hasStock;\n});\n\n// Usage\ncartSignals.itemAdded.dispatch({ productId: 'laptop-123', quantity: 1 });\n\nconst canProceed = await cartSignals.checkoutStarted.dispatch({ \n  cartTotal: 1299.99 \n});\n```\n\n### Game Event System\n```ts\ninterface GameEvents {\n  playerMoved: Signal<{ x: number, y: number, playerId: string }>;\n  enemyDefeated: Signal<{ enemyType: string, experience: number }>;\n  levelCompleted: Signal<{ level: number, score: number }>;\n  gameOver: Signal<{ finalScore: number, reason: string }>;\n}\n\nconst game: GameEvents = {\n  playerMoved: new Signal(),\n  enemyDefeated: new Signal(),\n  levelCompleted: new Signal(),\n  gameOver: new Signal({ memoize: true }) // Remember game over state\n};\n\n// Multiple systems can react independently\ngame.enemyDefeated.add(({ experience }) => {\n  player.addExperience(experience);\n});\n\ngame.enemyDefeated.add(({ enemyType }) => {\n  achievements.checkEnemyKillAchievements(enemyType);\n});\n\ngame.enemyDefeated.add(({ enemyType }) => {\n  audioSystem.playSound(`${enemyType}_death`);\n});\n\n// Priority-based listeners (higher priority = earlier execution)\ngame.levelCompleted.add(() => {\n  saveGameState(); // Critical - save first\n}, null, 10);\n\ngame.levelCompleted.add(() => {\n  showLevelCompleteAnimation(); // Visual feedback after save\n}, null, 5);\n```\n\n### Form Validation Pipeline\n```ts\ninterface ValidationResult {\n  isValid: boolean;\n  errors: string[];\n}\n\nconst formValidation = new Signal<\n  { field: string, value: any }, \n  ValidationResult\n>({ \n  propagate: 'any-fail', // Stop on first validation failure\n  listenerSuccessTest: (result: ValidationResult) => result.isValid \n});\n\n// Add validators with priority\nformValidation.add(({ field, value }) => {\n  // Required field validation (highest priority)\n  if (!value || value.trim() === '') {\n    return { isValid: false, errors: [`${field} is required`] };\n  }\n  return { isValid: true, errors: [] };\n}, null, 10);\n\nformValidation.add(({ field, value }) => {\n  // Email format validation\n  if (field === 'email' && !isValidEmail(value)) {\n    return { isValid: false, errors: ['Invalid email format'] };\n  }\n  return { isValid: true, errors: [] };\n}, null, 5);\n\n// Usage\nconst result = await formValidation.dispatch({ \n  field: 'email', \n  value: 'user@example.com' \n});\n\nif (!result.isValid) {\n  displayErrors(result.errors);\n}\n```\n\n## Advanced Configuration\n\n### Propagation Control\n```ts\n// Stop after first successful response\nconst findHandler = new Signal<string, boolean>({ \n  propagate: 'any',\n  haltOnResolve: true \n});\n\n// Stop after first failure\nconst validateAll = new Signal<any, boolean>({ \n  propagate: 'any-fail',\n  haltOnResolve: true,\n  listenerSuccessTest: (result) => result === true\n});\n\n// Wait for all listeners regardless of results\nconst notifyAll = new Signal<string>({ \n  propagate: 'all',\n  haltOnResolve: false \n});\n```\n\n### Listener Management\n```ts\nconst signal = new Signal<string>();\n\n// Add listener with binding control\nconst binding = signal.add((message) => {\n  console.log(message);\n}, null, 5); // priority 5\n\n// Temporarily suspend\nbinding.suspend();\nsignal.dispatch('This will not be logged');\n\n// Resume\nbinding.resume();\nsignal.dispatch('This will be logged');\n\n// Permanently remove\nbinding.detach();\nsignal.dispatch('This will not be logged');\n\n// Check if listener exists\nconst hasListener = signal.has(myFunction, myContext);\n```\n\n### Signal Lifecycle\n```ts\nconst signal = new Signal<string>({ memoize: true });\n\n// Dispatch early\nsignal.dispatch('Early message');\n\n// Late subscriber still receives the message\nsignal.addOnce((message) => {\n  console.log(message); // Logs: \"Early message\"\n});\n\n// Clear memoized value\nsignal.forget();\n\n// Suspend entire signal\nsignal.suspend();\nsignal.dispatch('Will be rejected'); // Promise rejection\n\nsignal.resume();\nsignal.dispatch('Will work'); // Normal dispatch\n\n// Clean up\nsignal.dispose(); // Removes all listeners and clears memory\n```\n\n## TypeScript Integration\n\nSignals provides excellent TypeScript support with full type inference:\n\n```ts\n// Payload and return types are fully typed\nconst typedSignal = new Signal<\n  { userId: string, action: 'login' | 'logout' },\n  { success: boolean, timestamp: number }\n>();\n\n// TypeScript knows the parameter types\ntypedSignal.add((data) => {\n  // data is typed as { userId: string, action: 'login' | 'logout' }\n  return { \n    success: true, \n    timestamp: Date.now() \n  }; // Return type is enforced\n});\n\n// Dispatch is type-safe\nconst result = await typedSignal.dispatch({\n  userId: '123',\n  action: 'login' // Only 'login' or 'logout' allowed\n});\n\n// Result is typed as { success: boolean, timestamp: number }\nconsole.log(result.timestamp);\n```\n\n## Performance Considerations\n\n- **Lightweight**: Zero dependencies, minimal runtime overhead\n- **Efficient**: Listeners are sorted by priority only when needed\n- **Memory-conscious**: Automatic cleanup of one-time listeners\n- **Async-optimized**: Non-blocking execution with proper Promise handling\n\n## Migration Guide\n\n### From v1.0.4 to v1.0.5\nThe main breaking change is in return value handling:\n\n```ts\n// v1.0.4 - always returned array\nconst results = await signal.dispatch(); // Always array\n\n// v1.0.5 - single value when one listener\nconst result = await signal.dispatch(); // Single value if one listener\nconst results = await signal.dispatch(); // Array if multiple listeners\n```\n\n## Contributing\n\nContributions are welcome! Please feel free to submit issues, feature requests, or pull requests.\n\n## License\n\n[Apache 2.0 License](https://opensource.org/license/apache-2-0/)\n\n## Running Tests\n\n```bash\nyarn install\nyarn test\n```\n","readmeFilename":"README.md","_rev":"1-c5dfd16c5a09b839a99430a1f5ff7f4b"}