@mmstack/primitives 20.4.4 → 20.4.5

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.
@@ -1,4 +1,5 @@
1
- import { untracked, signal, inject, DestroyRef, computed, PLATFORM_ID, isSignal, effect, ElementRef, linkedSignal, isDevMode, Injector, runInInjectionContext } from '@angular/core';
1
+ import * as i0 from '@angular/core';
2
+ import { untracked, signal, inject, DestroyRef, computed, PLATFORM_ID, isSignal, effect, ElementRef, linkedSignal, isDevMode, Injectable, Injector, runInInjectionContext } from '@angular/core';
2
3
  import { isPlatformServer } from '@angular/common';
3
4
  import { SIGNAL } from '@angular/core/primitives/signals';
4
5
 
@@ -1293,6 +1294,122 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
1293
1294
  return writable;
1294
1295
  }
1295
1296
 
1297
+ class MessageBus {
1298
+ channel = new BroadcastChannel('mmstack-tab-sync-bus');
1299
+ listeners = new Map();
1300
+ subscribe(id, listener) {
1301
+ this.unsubscribe(id); // Ensure no duplicate listeners
1302
+ const wrapped = (ev) => {
1303
+ try {
1304
+ if (ev.data?.id === id)
1305
+ listener(ev.data?.value);
1306
+ }
1307
+ catch {
1308
+ // noop
1309
+ }
1310
+ };
1311
+ this.channel.addEventListener('message', wrapped);
1312
+ this.listeners.set(id, wrapped);
1313
+ return {
1314
+ unsub: (() => this.unsubscribe(id)).bind(this),
1315
+ post: ((value) => this.channel.postMessage({ id, value })).bind(this),
1316
+ };
1317
+ }
1318
+ unsubscribe(id) {
1319
+ const listener = this.listeners.get(id);
1320
+ if (!listener)
1321
+ return;
1322
+ this.channel.removeEventListener('message', listener);
1323
+ this.listeners.delete(id);
1324
+ }
1325
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1326
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: MessageBus, providedIn: 'root' });
1327
+ }
1328
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: MessageBus, decorators: [{
1329
+ type: Injectable,
1330
+ args: [{
1331
+ providedIn: 'root',
1332
+ }]
1333
+ }] });
1334
+ function generateDeterministicID() {
1335
+ const stack = new Error().stack;
1336
+ if (stack) {
1337
+ // Look for the actual caller (first non-internal frame)
1338
+ const lines = stack.split('\n');
1339
+ for (let i = 2; i < lines.length; i++) {
1340
+ const line = lines[i];
1341
+ if (line && !line.includes('tabSync') && !line.includes('MessageBus')) {
1342
+ let hash = 0;
1343
+ for (let j = 0; j < line.length; j++) {
1344
+ const char = line.charCodeAt(j);
1345
+ hash = (hash << 5) - hash + char;
1346
+ hash = hash & hash;
1347
+ }
1348
+ return `auto-${Math.abs(hash)}`;
1349
+ }
1350
+ }
1351
+ }
1352
+ throw new Error('Could not generate deterministic ID, please provide one manually.');
1353
+ }
1354
+ /**
1355
+ * Synchronizes a WritableSignal across browser tabs using BroadcastChannel API.
1356
+ *
1357
+ * Creates a shared signal that automatically syncs its value between all tabs
1358
+ * of the same application. When the signal is updated in one tab, all other
1359
+ * tabs will receive the new value automatically.
1360
+ *
1361
+ * @template T - The type of the WritableSignal
1362
+ * @param sig - The WritableSignal to synchronize across tabs
1363
+ * @param opt - Optional configuration object
1364
+ * @param opt.id - Explicit channel ID for synchronization. If not provided,
1365
+ * a deterministic ID is generated based on the call site.
1366
+ * Use explicit IDs in production for reliability.
1367
+ *
1368
+ * @returns The same WritableSignal instance, now synchronized across tabs
1369
+ *
1370
+ * @throws {Error} When deterministic ID generation fails and no explicit ID is provided
1371
+ *
1372
+ * @example
1373
+ * ```typescript
1374
+ * // Basic usage - auto-generates channel ID from call site
1375
+ * const theme = tabSync(signal('dark'));
1376
+ *
1377
+ * // With explicit ID (recommended for production)
1378
+ * const userPrefs = tabSync(signal({ lang: 'en' }), { id: 'user-preferences' });
1379
+ *
1380
+ * // Changes in one tab will sync to all other tabs
1381
+ * theme.set('light'); // All tabs will update to 'light'
1382
+ * ```
1383
+ *
1384
+ * @remarks
1385
+ * - Only works in browser environments (returns original signal on server)
1386
+ * - Uses a single BroadcastChannel for all synchronized signals
1387
+ * - Automatically cleans up listeners when the injection context is destroyed
1388
+ * - Initial signal value after sync setup is not broadcasted to prevent loops
1389
+ *
1390
+ */
1391
+ function tabSync(sig, opt) {
1392
+ if (isPlatformServer(inject(PLATFORM_ID)))
1393
+ return sig;
1394
+ const id = opt?.id || generateDeterministicID();
1395
+ const bus = inject(MessageBus);
1396
+ const { unsub, post } = bus.subscribe(id, (next) => sig.set(next));
1397
+ let first = false;
1398
+ const effectRef = effect(() => {
1399
+ const val = sig();
1400
+ if (!first) {
1401
+ first = true;
1402
+ return;
1403
+ }
1404
+ post(val);
1405
+ }, ...(ngDevMode ? [{ debugName: "effectRef" }] : []));
1406
+ inject(DestroyRef).onDestroy(() => {
1407
+ effectRef.destroy();
1408
+ unsub();
1409
+ });
1410
+ return sig;
1411
+ }
1412
+
1296
1413
  function until(sourceSignal, predicate, options = {}) {
1297
1414
  const injector = options.injector ?? inject(Injector);
1298
1415
  return new Promise((resolve, reject) => {
@@ -1491,5 +1608,5 @@ function withHistory(source, opt) {
1491
1608
  * Generated bundle index. Do not edit.
1492
1609
  */
1493
1610
 
1494
- export { combineWith, debounce, debounced, derived, distinct, elementVisibility, filter, isDerivation, isMutable, map, mapArray, mediaQuery, mousePosition, mutable, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, stored, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toWritable, until, windowSize, withHistory };
1611
+ export { combineWith, debounce, debounced, derived, distinct, elementVisibility, filter, isDerivation, isMutable, map, mapArray, mediaQuery, mousePosition, mutable, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toWritable, until, windowSize, withHistory };
1495
1612
  //# sourceMappingURL=mmstack-primitives.mjs.map