{"_id":"@byscott-io/where-is-waldo","_rev":"11-b5d4d8ea285cdd4a34023a4c86c4ad17","name":"@byscott-io/where-is-waldo","dist-tags":{"latest":"0.1.10"},"versions":{"0.1.0":{"name":"@byscott-io/where-is-waldo","version":"0.1.0","keywords":["actioncable","presence","websocket","react","rails","real-time"],"author":{"name":"Scott Gibson"},"license":"MIT","_id":"@byscott-io/where-is-waldo@0.1.0","maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"homepage":"https://github.com/byscott-io/where_is_waldo#readme","bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"dist":{"shasum":"4dbbc92932c48a337486f9b5952a8a62f21cc1e0","tarball":"https://registry.npmjs.org/@byscott-io/where-is-waldo/-/where-is-waldo-0.1.0.tgz","fileCount":15,"integrity":"sha512-rp60vsriyuPM+nnQIxFVvWSuL3nBPf3m2oDcyMImAifUK3VhnSjfpfRA1eQFZdJbPC+H39LGLQhQkGfleYLWxw==","signatures":[{"sig":"MEUCIQCiK9phMoJ57EFMP/eycRrovIxMPGuMv/9Uekq0p4x1WgIgTnjXvl+n+VZWg7Y0mUmXB4wGZuETF8444xPs6+vZYJ0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":222420},"main":"dist/index.js","module":"dist/index.esm.js","gitHead":"9dc51213d283de87ae0087e0d13b2e68453ba85f","scripts":{"lint":"eslint src --ext .js,.jsx","test":"vitest run","build":"rollup -c","server":"cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001","lint:fix":"eslint src --ext .js,.jsx --fix","test:e2e":"start-server-and-test server http://localhost:3001 cypress:run","test:watch":"vitest","cypress:run":"cypress run","cypress:open":"cypress open","server:setup":"cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate","test:e2e:open":"start-server-and-test server http://localhost:3001 cypress:open","prepublishOnly":"npm run build"},"_npmUser":{"name":"sevgibson","email":"sevgibson@gmail.com"},"repository":{"url":"git+https://github.com/byscott-io/where_is_waldo.git","type":"git"},"_npmVersion":"10.9.2","description":"Real-time presence tracking for ActionCable with React hooks","directories":{},"resolutions":{"qs":"^6.15.2"},"_nodeVersion":"22.13.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":"^19.2.7","eslint":"^8.50.0","rollup":"^4.62.2","vitest":"^4.1.10","cypress":"^15.18.1","react-dom":"^19.2.7","@babel/core":"^7.29.7","@babel/preset-env":"^7.29.7","@rails/actioncable":"^8.1.300","@babel/preset-react":"^7.29.7","eslint-plugin-react":"^7.33.0","@rollup/plugin-babel":"^7.1.0","@testing-library/dom":"^10.4.1","start-server-and-test":"^3.0.11","@testing-library/react":"^16.3.2","@rollup/plugin-commonjs":"^29.0.3","eslint-plugin-react-hooks":"^7.1.1","@rollup/plugin-node-resolve":"^16.0.3"},"peerDependencies":{"react":"^17.0.0 || ^18.0.0 || ^19.0.0","@rails/actioncable":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/where-is-waldo_0.1.0_1784125696090_0.7234664639321406","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@byscott-io/where-is-waldo","version":"0.1.1","keywords":["actioncable","presence","websocket","react","rails","real-time"],"author":{"name":"Scott Gibson"},"license":"MIT","_id":"@byscott-io/where-is-waldo@0.1.1","maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"homepage":"https://github.com/byscott-io/where_is_waldo#readme","bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"dist":{"shasum":"e2f43b54306899bc1eb9350086e5b3af7dbc3600","tarball":"https://registry.npmjs.org/@byscott-io/where-is-waldo/-/where-is-waldo-0.1.1.tgz","fileCount":15,"integrity":"sha512-GVjULB9PH7TN8QAF4cs+onvndp7PamqhY4YxHUWPH7OvO84lAvWZ5jtAQVCt1pcqqHNUUTMWiLuaRbzZb+Lr8A==","signatures":[{"sig":"MEUCIQDX0WuJTT2gwRBjYjCvQKYivhpWLFKu8ffyBnfcIFjPqgIgPMZVsBs4Zk5M1ueuM1Ljt5M0ILRfRkDlx6/je+8AZJ8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":222460},"main":"dist/index.js","module":"dist/index.esm.js","gitHead":"be1aec8cc75e2a1eea62f9b797e1bfe130b70629","scripts":{"lint":"eslint src --ext .js,.jsx","test":"vitest run","build":"rollup -c","server":"cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001","lint:fix":"eslint src --ext .js,.jsx --fix","test:e2e":"start-server-and-test server http://localhost:3001 cypress:run","test:watch":"vitest","cypress:run":"cypress run","cypress:open":"cypress open","server:setup":"cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate","test:e2e:open":"start-server-and-test server http://localhost:3001 cypress:open","prepublishOnly":"npm run build"},"_npmUser":{"name":"sevgibson","email":"sevgibson@gmail.com"},"repository":{"url":"git+https://github.com/byscott-io/where_is_waldo.git","type":"git"},"_npmVersion":"10.9.2","description":"Real-time presence tracking for ActionCable with React hooks","directories":{},"resolutions":{"qs":"^6.15.2"},"_nodeVersion":"22.13.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":"^19.2.7","eslint":"^8.50.0","rollup":"^4.62.2","vitest":"^4.1.10","cypress":"^15.18.1","react-dom":"^19.2.7","@babel/core":"^7.29.7","@babel/preset-env":"^7.29.7","@rails/actioncable":"^8.1.300","@babel/preset-react":"^7.29.7","eslint-plugin-react":"^7.33.0","@rollup/plugin-babel":"^7.1.0","@testing-library/dom":"^10.4.1","start-server-and-test":"^3.0.11","@testing-library/react":"^16.3.2","@rollup/plugin-commonjs":"^29.0.3","eslint-plugin-react-hooks":"^7.1.1","@rollup/plugin-node-resolve":"^16.0.3"},"peerDependencies":{"react":"^17.0.0 || ^18.0.0 || ^19.0.0","@rails/actioncable":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/where-is-waldo_0.1.1_1784134239593_0.3717032355552503","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@byscott-io/where-is-waldo","version":"0.1.2","keywords":["actioncable","presence","websocket","react","rails","real-time"],"author":{"name":"Scott Gibson"},"license":"MIT","_id":"@byscott-io/where-is-waldo@0.1.2","maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"homepage":"https://github.com/byscott-io/where_is_waldo#readme","bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"dist":{"shasum":"b3206316d421d57066e30b307d59a3f45c8ee90d","tarball":"https://registry.npmjs.org/@byscott-io/where-is-waldo/-/where-is-waldo-0.1.2.tgz","fileCount":15,"integrity":"sha512-a/qJMJTczS0+nkWsMbX2dRJxa3J1RgESv3KWwZU6e/CXUKQElq+FfYWr6SWnCOmXifMWZVkBPHsYHuIZDD8GmQ==","signatures":[{"sig":"MEQCIG1aa2YkOIp4G1ukOcubvUv7hJWeYFyymwMBm4gPw5rVAiBj5ZOnB40oYn/bmAkJvfeelWcm2l9hyXpyBsbST/WDjg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":169175},"main":"dist/index.js","module":"dist/index.esm.js","gitHead":"9e4c95ee83766f87986dda10afcdce5f7ff040d6","scripts":{"lint":"eslint src --ext .js,.jsx","test":"vitest run","build":"rollup -c","server":"cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001","lint:fix":"eslint src --ext .js,.jsx --fix","test:e2e":"start-server-and-test server http://localhost:3001 cypress:run","test:watch":"vitest","cypress:run":"cypress run","cypress:open":"cypress open","server:setup":"cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate","test:e2e:open":"start-server-and-test server http://localhost:3001 cypress:open","prepublishOnly":"npm run build"},"_npmUser":{"name":"sevgibson","email":"sevgibson@gmail.com"},"repository":{"url":"git+https://github.com/byscott-io/where_is_waldo.git","type":"git"},"_npmVersion":"10.9.2","description":"Real-time presence tracking for ActionCable with React hooks","directories":{},"resolutions":{"qs":"^6.15.2","axios":"^1.18.0","systeminformation":"^5.31.17"},"_nodeVersion":"22.13.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":"^19.2.7","eslint":"^8.50.0","rollup":"^4.62.2","vitest":"^4.1.10","cypress":"^15.18.1","esbuild":"^0.28.1","react-dom":"^19.2.7","@rails/actioncable":"^8.1.300","eslint-plugin-react":"^7.33.0","@testing-library/dom":"^10.4.1","rollup-plugin-esbuild":"^6.2.1","start-server-and-test":"^3.0.11","@testing-library/react":"^16.3.2","@rollup/plugin-commonjs":"^29.0.3","eslint-plugin-react-hooks":"^7.1.1","@rollup/plugin-node-resolve":"^16.0.3"},"peerDependencies":{"react":"^17.0.0 || ^18.0.0 || ^19.0.0","@rails/actioncable":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/where-is-waldo_0.1.2_1784818978713_0.00309268439192234","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@byscott-io/where-is-waldo","version":"0.1.3","keywords":["actioncable","presence","websocket","react","rails","real-time"],"author":{"name":"Scott Gibson"},"license":"MIT","_id":"@byscott-io/where-is-waldo@0.1.3","maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"homepage":"https://github.com/byscott-io/where_is_waldo#readme","bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"dist":{"shasum":"03e749e75e4b0ce8f3d915eec3f70df6837a07d8","tarball":"https://registry.npmjs.org/@byscott-io/where-is-waldo/-/where-is-waldo-0.1.3.tgz","fileCount":15,"integrity":"sha512-rFx9X8H8pi81gbAY96i3qIHMNQQYlEyrkQaWqeWJCCJu6h6uCYI4T7Dv5192+noDo5Bj8zYjDzRIFgeJ8edqEQ==","signatures":[{"sig":"MEQCIGXvI/YwDvSXuGBgmjC+Wle584kcwMuVcE2dL2Ur+8JpAiBPWwAnaeiX+aligiy1BQjWpeYcUFzBN/DvkfxYXNl0qg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":169219},"main":"dist/index.js","module":"dist/index.esm.js","gitHead":"e88a9f3a114dad577bc59cdba900bfde8f58b0ac","scripts":{"lint":"eslint src --ext .js,.jsx","test":"vitest run","build":"rollup -c","server":"cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001","lint:fix":"eslint src --ext .js,.jsx --fix","test:e2e":"start-server-and-test server http://localhost:3001 cypress:run","test:watch":"vitest","cypress:run":"cypress run","cypress:open":"cypress open","server:setup":"cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate","test:e2e:open":"start-server-and-test server http://localhost:3001 cypress:open","prepublishOnly":"npm run build"},"_npmUser":{"name":"sevgibson","email":"sevgibson@gmail.com"},"repository":{"url":"git+https://github.com/byscott-io/where_is_waldo.git","type":"git"},"_npmVersion":"10.9.2","description":"Real-time presence tracking for ActionCable with React hooks","directories":{},"resolutions":{"qs":"^6.15.2","axios":"^1.18.0","systeminformation":"^5.31.17","eslint/**/brace-expansion":"^1.1.16"},"_nodeVersion":"22.13.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":"^19.2.7","eslint":"^8.50.0","rollup":"^4.62.2","vitest":"^4.1.10","cypress":"^15.18.1","esbuild":"^0.28.1","react-dom":"^19.2.7","@rails/actioncable":"^8.1.300","eslint-plugin-react":"^7.33.0","@testing-library/dom":"^10.4.1","rollup-plugin-esbuild":"^6.2.1","start-server-and-test":"^3.0.11","@testing-library/react":"^16.3.2","@rollup/plugin-commonjs":"^29.0.3","eslint-plugin-react-hooks":"^7.1.1","@rollup/plugin-node-resolve":"^16.0.3"},"peerDependencies":{"react":"^17.0.0 || ^18.0.0 || ^19.0.0","@rails/actioncable":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/where-is-waldo_0.1.3_1784823469849_0.6566430827291194","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@byscott-io/where-is-waldo","version":"0.1.4","keywords":["actioncable","presence","websocket","react","rails","real-time"],"author":{"name":"Scott Gibson"},"license":"MIT","_id":"@byscott-io/where-is-waldo@0.1.4","maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"homepage":"https://github.com/byscott-io/where_is_waldo#readme","bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"dist":{"shasum":"0692b873ac0f859c65afe44075478918dfdd220b","tarball":"https://registry.npmjs.org/@byscott-io/where-is-waldo/-/where-is-waldo-0.1.4.tgz","fileCount":15,"integrity":"sha512-hgENNEKxQrgfwD2Cft2f2YHP2hk3RkMOGqnUMUw5i5vtS0ItxwXoWNArgPQ4S2KH8Mtzk7YpbNVnGOKZIQE6Vw==","signatures":[{"sig":"MEUCIAhLAgIZCQG35RDzlXOD7UGekm9JAGrVGerSRhVf9A9KAiEAo8/b2O+FkKsGoCjWEM8fT9tknEicuFCtl43sHjBMjyQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":169744},"main":"dist/index.js","module":"dist/index.esm.js","gitHead":"f1d65855d0fc4588046f88bec3696c3e1d066339","scripts":{"lint":"eslint src --ext .js,.jsx","test":"vitest run","build":"rollup -c","server":"cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001","lint:fix":"eslint src --ext .js,.jsx --fix","test:e2e":"start-server-and-test server http://localhost:3001 cypress:run","test:watch":"vitest","cypress:run":"cypress run","cypress:open":"cypress open","server:setup":"cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate","test:e2e:open":"start-server-and-test server http://localhost:3001 cypress:open","prepublishOnly":"npm run build"},"_npmUser":{"name":"sevgibson","email":"sevgibson@gmail.com"},"repository":{"url":"git+https://github.com/byscott-io/where_is_waldo.git","type":"git"},"_npmVersion":"10.9.2","description":"Real-time presence tracking for ActionCable with React hooks","directories":{},"resolutions":{"qs":"^6.15.2","axios":"^1.18.0","systeminformation":"^5.31.17","eslint/**/brace-expansion":"^1.1.16"},"_nodeVersion":"22.13.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":"^19.2.7","eslint":"^8.50.0","rollup":"^4.62.2","vitest":"^4.1.10","cypress":"^15.18.1","esbuild":"^0.28.1","react-dom":"^19.2.7","@rails/actioncable":"^8.1.300","eslint-plugin-react":"^7.33.0","@testing-library/dom":"^10.4.1","rollup-plugin-esbuild":"^6.2.1","start-server-and-test":"^3.0.11","@testing-library/react":"^16.3.2","@rollup/plugin-commonjs":"^29.0.3","eslint-plugin-react-hooks":"^7.1.1","@rollup/plugin-node-resolve":"^16.0.3"},"peerDependencies":{"react":"^17.0.0 || ^18.0.0 || ^19.0.0","@rails/actioncable":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/where-is-waldo_0.1.4_1784842748274_0.6391134020118654","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"name":"@byscott-io/where-is-waldo","version":"0.1.5","keywords":["actioncable","presence","websocket","react","rails","real-time"],"author":{"name":"Scott Gibson"},"license":"MIT","_id":"@byscott-io/where-is-waldo@0.1.5","maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"homepage":"https://github.com/byscott-io/where_is_waldo#readme","bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"dist":{"shasum":"91dac91ec4c2e74c670a2955cf140b6b8463d133","tarball":"https://registry.npmjs.org/@byscott-io/where-is-waldo/-/where-is-waldo-0.1.5.tgz","fileCount":15,"integrity":"sha512-R899bGa8cgUas/nXrjHYidCrQsSpJg2R1MpTHhdQZaWEMj0EBxQE90RT6FVzzOU0jT2HACKWEWXzEuyfakCWqA==","signatures":[{"sig":"MEYCIQDYp9JD0p7tVuED4UKivythfHjOLOVBzU32j/PEf5BYDwIhAIihYqQjbFnZEBwSsz+8Pv6gPZb86oKNVsuLb3DtHEpF","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":169744},"main":"dist/index.js","module":"dist/index.esm.js","gitHead":"c8f5a2d420f53fb7c4f78fb20d8df0be9e8c95c1","scripts":{"lint":"eslint src --ext .js,.jsx","test":"vitest run","build":"rollup -c","server":"cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001","lint:fix":"eslint src --ext .js,.jsx --fix","test:e2e":"start-server-and-test server http://localhost:3001 cypress:run","test:watch":"vitest","cypress:run":"cypress run","cypress:open":"cypress open","server:setup":"cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate","test:e2e:open":"start-server-and-test server http://localhost:3001 cypress:open","prepublishOnly":"npm run build"},"_npmUser":{"name":"sevgibson","email":"sevgibson@gmail.com"},"repository":{"url":"git+https://github.com/byscott-io/where_is_waldo.git","type":"git"},"_npmVersion":"10.9.2","description":"Real-time presence tracking for ActionCable with React hooks","directories":{},"resolutions":{"qs":"^6.15.2","axios":"^1.18.0","systeminformation":"^5.31.17","eslint/**/brace-expansion":"^1.1.16"},"_nodeVersion":"22.13.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":"^19.2.8","eslint":"^8.50.0","rollup":"^4.62.2","vitest":"^4.1.10","cypress":"^15.18.1","esbuild":"^0.28.1","react-dom":"^19.2.8","@rails/actioncable":"^8.1.300","eslint-plugin-react":"^7.33.0","@testing-library/dom":"^10.4.1","rollup-plugin-esbuild":"^6.2.1","start-server-and-test":"^3.0.11","@testing-library/react":"^16.3.2","@rollup/plugin-commonjs":"^29.0.3","eslint-plugin-react-hooks":"^7.1.1","@rollup/plugin-node-resolve":"^16.0.3"},"peerDependencies":{"react":"^17.0.0 || ^18.0.0 || ^19.0.0","@rails/actioncable":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/where-is-waldo_0.1.5_1784928398599_0.848078047058396","host":"s3://npm-registry-packages-npm-production"}},"0.1.6":{"name":"@byscott-io/where-is-waldo","version":"0.1.6","keywords":["actioncable","presence","websocket","react","rails","real-time"],"author":{"name":"Scott Gibson"},"license":"MIT","_id":"@byscott-io/where-is-waldo@0.1.6","maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"homepage":"https://github.com/byscott-io/where_is_waldo#readme","bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"dist":{"shasum":"35a34c1c72d69f69bcf74806c8c2bac81bc3da5a","tarball":"https://registry.npmjs.org/@byscott-io/where-is-waldo/-/where-is-waldo-0.1.6.tgz","fileCount":15,"integrity":"sha512-x8fRVBMgImCe6Ikn7/EP3Y34Z0LtNaDNKW8S98M7OhfChdciPS6aqj9W42Ns42ue3UFRZOkh0LzkZBo0OInzWg==","signatures":[{"sig":"MEYCIQCFtxzfGGzgKjkj2tATWso5Q6vIQRWctAn7OwMt0vq8sQIhAONKl7QnxmWh3mQ2KM2kISJ6ElqubZbd16IjgAr3HzUm","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":169744},"main":"dist/index.js","module":"dist/index.esm.js","gitHead":"fd56b2281c0db4a9b83b728646d278cd574585ce","scripts":{"lint":"eslint src --ext .js,.jsx","test":"vitest run","build":"rollup -c","server":"cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001","lint:fix":"eslint src --ext .js,.jsx --fix","test:e2e":"start-server-and-test server http://localhost:3001 cypress:run","test:watch":"vitest","cypress:run":"cypress run","cypress:open":"cypress open","server:setup":"cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate","test:e2e:open":"start-server-and-test server http://localhost:3001 cypress:open","prepublishOnly":"npm run build"},"_npmUser":{"name":"sevgibson","email":"sevgibson@gmail.com"},"repository":{"url":"git+https://github.com/byscott-io/where_is_waldo.git","type":"git"},"_npmVersion":"10.9.2","description":"Real-time presence tracking for ActionCable with React hooks","directories":{},"resolutions":{"qs":"^6.15.2","axios":"^1.18.0","systeminformation":"^5.31.17","eslint/**/brace-expansion":"^1.1.16"},"_nodeVersion":"22.13.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":"^19.2.8","eslint":"^8.50.0","rollup":"^4.62.2","vitest":"^4.1.10","cypress":"^15.18.1","esbuild":"^0.28.1","react-dom":"^19.2.8","@rails/actioncable":"^8.1.300","eslint-plugin-react":"^7.33.0","@testing-library/dom":"^10.4.1","rollup-plugin-esbuild":"^6.2.1","start-server-and-test":"^3.0.11","@testing-library/react":"^16.3.2","@rollup/plugin-commonjs":"^29.0.3","eslint-plugin-react-hooks":"^7.1.1","@rollup/plugin-node-resolve":"^16.0.3"},"peerDependencies":{"react":"^17.0.0 || ^18.0.0 || ^19.0.0","@rails/actioncable":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/where-is-waldo_0.1.6_1784990886441_0.8042348676118147","host":"s3://npm-registry-packages-npm-production"}},"0.1.7":{"name":"@byscott-io/where-is-waldo","version":"0.1.7","keywords":["actioncable","presence","websocket","react","rails","real-time"],"author":{"name":"Scott Gibson"},"license":"MIT","_id":"@byscott-io/where-is-waldo@0.1.7","maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"homepage":"https://github.com/byscott-io/where_is_waldo#readme","bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"dist":{"shasum":"58199874c5beb2f24f09b5a873105f665b7ba7d0","tarball":"https://registry.npmjs.org/@byscott-io/where-is-waldo/-/where-is-waldo-0.1.7.tgz","fileCount":15,"integrity":"sha512-lX4PYKAp5ZPuIkQrALuOaZKqLNjJI1D7H1gM4i0r0AlDJ5/l6LQW+Z7G/MFBCTNQF/Xy4TZdaGY22b8s9HsMpg==","signatures":[{"sig":"MEUCIQDiYesLYs8XMpKEcjLR9Ec71vUo6kG++5S4cL0+IV1F3gIgTxdTMZG1n6GTG2L7NGE4s0641RWsVfMZU7ZJXEpQdVs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":170242},"main":"dist/index.js","module":"dist/index.esm.js","gitHead":"35a97c9f8d24427209a7f97eb3db84e425777d5c","scripts":{"lint":"eslint src --ext .js,.jsx","test":"vitest run","build":"rollup -c","server":"cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001","lint:fix":"eslint src --ext .js,.jsx --fix","test:e2e":"start-server-and-test server http://localhost:3001 cypress:run","test:watch":"vitest","cypress:run":"cypress run","cypress:open":"cypress open","server:setup":"cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate","test:e2e:open":"start-server-and-test server http://localhost:3001 cypress:open","prepublishOnly":"npm run build"},"_npmUser":{"name":"sevgibson","email":"sevgibson@gmail.com"},"repository":{"url":"git+https://github.com/byscott-io/where_is_waldo.git","type":"git"},"_npmVersion":"10.9.8","description":"Real-time presence tracking for ActionCable with React hooks","directories":{},"resolutions":{"qs":"^6.15.2","axios":"^1.18.0","systeminformation":"^5.31.17","eslint/**/brace-expansion":"^1.1.16"},"_nodeVersion":"22.23.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":"^19.2.8","eslint":"^8.50.0","rollup":"^4.62.2","vitest":"^4.1.10","cypress":"^15.18.1","esbuild":"^0.28.1","react-dom":"^19.2.8","@rails/actioncable":"^8.1.300","eslint-plugin-react":"^7.33.0","@testing-library/dom":"^10.4.1","rollup-plugin-esbuild":"^6.2.1","start-server-and-test":"^3.0.11","@testing-library/react":"^16.3.2","@rollup/plugin-commonjs":"^29.0.3","eslint-plugin-react-hooks":"^7.1.1","@rollup/plugin-node-resolve":"^16.0.3"},"peerDependencies":{"react":"^17.0.0 || ^18.0.0 || ^19.0.0","@rails/actioncable":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/where-is-waldo_0.1.7_1785164588617_0.17162903136250374","host":"s3://npm-registry-packages-npm-production"}},"0.1.8":{"name":"@byscott-io/where-is-waldo","version":"0.1.8","keywords":["actioncable","presence","websocket","react","rails","real-time"],"author":{"name":"Scott Gibson"},"license":"MIT","_id":"@byscott-io/where-is-waldo@0.1.8","maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"homepage":"https://github.com/byscott-io/where_is_waldo#readme","bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"dist":{"shasum":"9c7ad20fa60ff00f9ca62cf0fd02f35d59eacad6","tarball":"https://registry.npmjs.org/@byscott-io/where-is-waldo/-/where-is-waldo-0.1.8.tgz","fileCount":15,"integrity":"sha512-E5gpYBrw8gvoKaC6sNwwlVqfhUlFK7r3n2KFasy8M8Q/Yrvo5CaZUjPD+h47ag6xJrtL+P3UxCqkAFpbAvxXzg==","signatures":[{"sig":"MEUCIQCUPyx+e7hxWzTiqM7lejLUUlZJzNeYMeey9RhTi4LZyQIgM5LqP3hjLdW+TbgEqFv6z+uV3vXIbyHKS7Np9il4Qwg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":170651},"main":"dist/index.js","module":"dist/index.esm.js","gitHead":"2978cdcaed585be4fe417240cffaa078620286ae","scripts":{"lint":"eslint src --ext .js,.jsx","test":"vitest run","build":"rollup -c","server":"cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001","lint:fix":"eslint src --ext .js,.jsx --fix","test:e2e":"start-server-and-test server http://localhost:3001 cypress:run","test:watch":"vitest","cypress:run":"cypress run","cypress:open":"cypress open","server:setup":"cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate","test:e2e:open":"start-server-and-test server http://localhost:3001 cypress:open","prepublishOnly":"npm run build"},"_npmUser":{"name":"sevgibson","email":"sevgibson@gmail.com"},"repository":{"url":"git+https://github.com/byscott-io/where_is_waldo.git","type":"git"},"_npmVersion":"10.9.8","description":"Real-time presence tracking for ActionCable with React hooks","directories":{},"resolutions":{"qs":"^6.15.2","axios":"^1.18.0","systeminformation":"^5.31.17","eslint/**/brace-expansion":"^1.1.16"},"_nodeVersion":"22.23.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":"^19.2.8","eslint":"^8.50.0","rollup":"^4.62.2","vitest":"^4.1.10","cypress":"^15.18.1","esbuild":"^0.28.1","react-dom":"^19.2.8","@rails/actioncable":"^8.1.300","eslint-plugin-react":"^7.33.0","@testing-library/dom":"^10.4.1","rollup-plugin-esbuild":"^6.2.1","start-server-and-test":"^3.0.11","@testing-library/react":"^16.3.2","@rollup/plugin-commonjs":"^29.0.3","eslint-plugin-react-hooks":"^7.1.1","@rollup/plugin-node-resolve":"^16.0.3"},"peerDependencies":{"react":"^17.0.0 || ^18.0.0 || ^19.0.0","@rails/actioncable":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/where-is-waldo_0.1.8_1785377168225_0.07355814167414332","host":"s3://npm-registry-packages-npm-production"}},"0.1.9":{"name":"@byscott-io/where-is-waldo","version":"0.1.9","keywords":["actioncable","presence","websocket","react","rails","real-time"],"author":{"name":"Scott Gibson"},"license":"MIT","_id":"@byscott-io/where-is-waldo@0.1.9","maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"homepage":"https://github.com/byscott-io/where_is_waldo#readme","bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"dist":{"shasum":"c7a295f6ce86a6fe8bcad6341b10c302bee47200","tarball":"https://registry.npmjs.org/@byscott-io/where-is-waldo/-/where-is-waldo-0.1.9.tgz","fileCount":15,"integrity":"sha512-zmWLLwu//qSqj+MweCfmdsLRowKZmltFSaNIuHOxBkqq0xh4+QQUfeM6moOBNkD0j64Uus6OUxtprjHM9aOrmA==","signatures":[{"sig":"MEYCIQDCd6f+/gzNZqG8HdNnkvQwaDlTzaHNJ70lF8ijIjOJvQIhAPvkUoLSrk4J8Ubx7cwU1KHtStVekn1mLEGmUQyf8f/e","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":170727},"main":"dist/index.js","module":"dist/index.esm.js","gitHead":"707f75b1fad6b86bbb8c46d79d56caf6263818de","scripts":{"lint":"eslint src --ext .js,.jsx","test":"vitest run","build":"rollup -c","server":"cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001","lint:fix":"eslint src --ext .js,.jsx --fix","test:e2e":"start-server-and-test server http://localhost:3001 cypress:run","test:watch":"vitest","cypress:run":"cypress run","cypress:open":"cypress open","server:setup":"cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate","test:e2e:open":"start-server-and-test server http://localhost:3001 cypress:open","prepublishOnly":"npm run build"},"_npmUser":{"name":"sevgibson","email":"sevgibson@gmail.com"},"repository":{"url":"git+https://github.com/byscott-io/where_is_waldo.git","type":"git"},"_npmVersion":"10.9.8","description":"Real-time presence tracking for ActionCable with React hooks","directories":{},"resolutions":{"qs":"^6.15.2","axios":"^1.18.0","undici":"^7.29.0","js-yaml":"^4.3.1","postcss":"^8.5.26","systeminformation":"^5.31.17","eslint/**/brace-expansion":"^1.1.16"},"_nodeVersion":"22.23.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":"^19.2.8","eslint":"^8.50.0","rollup":"^4.62.3","vitest":"^4.1.10","cypress":"^15.18.1","esbuild":"^0.28.1","react-dom":"^19.2.8","@rails/actioncable":"^8.1.300","eslint-plugin-react":"^7.33.0","@testing-library/dom":"^10.4.1","rollup-plugin-esbuild":"^6.2.1","start-server-and-test":"^3.0.11","@testing-library/react":"^16.3.2","@rollup/plugin-commonjs":"^29.0.3","eslint-plugin-react-hooks":"^7.1.1","@rollup/plugin-node-resolve":"^16.0.3"},"peerDependencies":{"react":"^17.0.0 || ^18.0.0 || ^19.0.0","@rails/actioncable":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/where-is-waldo_0.1.9_1786109156995_0.13670899244209744","host":"s3://npm-registry-packages-npm-production"}},"0.1.10":{"name":"@byscott-io/where-is-waldo","version":"0.1.10","description":"Real-time presence tracking for ActionCable with React hooks","main":"dist/index.js","module":"dist/index.esm.js","scripts":{"build":"rollup -c","test":"vitest run","test:watch":"vitest","lint":"eslint src --ext .js,.jsx","lint:fix":"eslint src --ext .js,.jsx --fix","prepublishOnly":"npm run build","cypress:open":"cypress open","cypress:run":"cypress run","server":"cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001","server:setup":"cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate","test:e2e":"start-server-and-test server http://localhost:3001 cypress:run","test:e2e:open":"start-server-and-test server http://localhost:3001 cypress:open"},"peerDependencies":{"@rails/actioncable":"^7.0.0 || ^8.0.0","react":"^17.0.0 || ^18.0.0 || ^19.0.0"},"devDependencies":{"@rails/actioncable":"^8.1.300","@rollup/plugin-commonjs":"^29.0.3","@rollup/plugin-node-resolve":"^16.0.3","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.2","cypress":"^15.18.1","esbuild":"^0.28.1","eslint":"^8.50.0","eslint-plugin-react":"^7.33.0","eslint-plugin-react-hooks":"^7.1.1","jsdom":"^29.1.1","react":"^19.2.8","react-dom":"^19.2.8","rollup":"^4.62.3","rollup-plugin-esbuild":"^6.2.1","start-server-and-test":"^3.0.11","vitest":"^4.1.10"},"repository":{"type":"git","url":"git+https://github.com/byscott-io/where_is_waldo.git"},"keywords":["actioncable","presence","websocket","react","rails","real-time"],"author":{"name":"Scott Gibson"},"license":"MIT","bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"homepage":"https://github.com/byscott-io/where_is_waldo#readme","publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"resolutions":{"axios":"^1.18.0","eslint/**/brace-expansion":"^1.1.16","js-yaml":"^4.3.1","postcss":"^8.5.26","qs":"^6.15.2","undici":"^7.29.0","systeminformation":"^5.31.17"},"_id":"@byscott-io/where-is-waldo@0.1.10","gitHead":"dc22d0f10557f172793b0f7fbf7b81aeb8ce386d","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-0s+ueB19cmM1zj5nUgQpfXpCTaVrl333DmPQzS+9nrPsBDHVxWUZFnIdnIhwN6gksCV+S7aUNtNuVVaIqe8sXg==","shasum":"fc412bbb7c7e18392f171ee917fc6ad15a6e987a","tarball":"https://registry.npmjs.org/@byscott-io/where-is-waldo/-/where-is-waldo-0.1.10.tgz","fileCount":15,"unpackedSize":171205,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICuPNdf47BLeyxJlM/ZZRaN0Lv7lCyxioae4Kl5VQzZVAiANi96lfHc8pZETd5n/Ock4KUyhS0xIXzp55HcUZUwLCw=="}]},"_npmUser":{"name":"sevgibson","email":"sevgibson@gmail.com"},"directories":{},"maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/where-is-waldo_0.1.10_1786109697991_0.05244528905802781"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-15T14:28:15.936Z","modified":"2026-08-07T13:34:58.349Z","0.1.0":"2026-07-15T14:28:16.240Z","0.1.1":"2026-07-15T16:50:39.726Z","0.1.2":"2026-07-23T15:02:58.865Z","0.1.3":"2026-07-23T16:17:49.995Z","0.1.4":"2026-07-23T21:39:08.411Z","0.1.5":"2026-07-24T21:26:38.745Z","0.1.6":"2026-07-25T14:48:06.581Z","0.1.7":"2026-07-27T15:03:08.766Z","0.1.8":"2026-07-30T02:06:08.428Z","0.1.9":"2026-08-07T13:25:57.153Z","0.1.10":"2026-08-07T13:34:58.164Z"},"bugs":{"url":"https://github.com/byscott-io/where_is_waldo/issues"},"author":{"name":"Scott Gibson"},"license":"MIT","homepage":"https://github.com/byscott-io/where_is_waldo#readme","keywords":["actioncable","presence","websocket","react","rails","real-time"],"repository":{"type":"git","url":"git+https://github.com/byscott-io/where_is_waldo.git"},"description":"Real-time presence tracking for ActionCable with React hooks","maintainers":[{"name":"sevgibson","email":"sevgibson@gmail.com"}],"readme":"# WhereIsWaldo\n\nReal-time presence tracking for Rails + React using ActionCable.\n\n[![CI](https://github.com/byscott-io/where_is_waldo/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/byscott-io/where_is_waldo/actions/workflows/ci.yml)\n[![Gem Version](https://img.shields.io/gem/v/where_is_waldo)](https://rubygems.org/gems/where_is_waldo)\n[![npm version](https://img.shields.io/npm/v/@byscott-io/where-is-waldo)](https://www.npmjs.com/package/@byscott-io/where-is-waldo)\n[![License](https://img.shields.io/github/license/byscott-io/where_is_waldo)](LICENSE)\n\n## Features\n\n- **Presence tracking** - know who's online\n- **Live presence roster** - per-device \"who's around\" (active/idle/background across web + mobile), pluggable per-account delivery (`:poll` / `:nudge` / `:fanout` / `:broadcast`)\n- **Scope-based queries** - `online(org.users.admin)`\n- **Targeted broadcasting** - send to any AR scope\n- **Event subscriptions** - components subscribe by event type (`useWaldoEvent`)\n- **Activity monitoring** - tab visibility, user activity\n- **Multi-session** - same user, multiple tabs/devices\n- **Flexible storage** - database or Redis\n\n## Quickstart\n\n### 1. Install\n\n```ruby\n# Gemfile — published on RubyGems.org\ngem 'where_is_waldo', '~> 0.1'\n```\n\n```bash\n# Database adapter (default)\nrails generate where_is_waldo:install --subject_column=user_id\nrails db:migrate\n\n# Redis adapter (no migration needed)\nrails generate where_is_waldo:install --adapter=redis --subject_column=user_id\n```\n\n```bash\n# published on npm (public registry)\nnpm install @byscott-io/where-is-waldo @rails/actioncable\n```\n\n### 2. Configure\n\n```ruby\n# config/initializers/where_is_waldo.rb\nWhereIsWaldo.configure do |config|\n  config.subject_class = \"User\"\n  config.authenticate_proc = ->(request) {\n    # Return user_id from your auth token\n    decode_token(request.params[:token])[:user_id]\n  }\nend\n```\n\n```jsx\n// app.jsx — configure the connection once, wrap the app\nimport { configureCable, PresenceProvider } from '@byscott-io/where-is-waldo';\n\nconfigureCable({\n  url: '/cable',\n  getToken: () => localStorage.getItem('token'),\n  presence: {\n    debug: true,  // Enable console logging for troubleshooting\n  },\n});\n\n<PresenceProvider>\n  <App />\n</PresenceProvider>\n```\n\n```jsx\n// any component — subscribe to an event type and filter the payload yourself.\n// Auto-unsubscribes on unmount; no central handler config.\nimport { useWaldoEvent } from '@byscott-io/where-is-waldo';\n\nfunction Notifications() {\n  useWaldoEvent('notification', (data) => showToast(data.message));\n  useWaldoEvent('force_logout', () => logout());\n  return null;\n}\n```\n\n### 3. Use\n\n```ruby\n# Query who's online\nWhereIsWaldo.online(org.users)           # => AR relation\nWhereIsWaldo.online(org.users.admin)     # => filter by scope\nWhereIsWaldo.subject_online?(user.id)    # => true/false\n\n# Broadcast messages\nWhereIsWaldo.broadcast_to(org.users, :notification, { message: \"Hello!\" })\nWhereIsWaldo.broadcast_to(user, :force_logout, { reason: \"Password changed\" })\n```\n\n---\n\n## Detailed Documentation\n\n### Server Configuration\n\n```ruby\nWhereIsWaldo.configure do |config|\n  config.adapter = :database            # or :redis\n  config.table_name = \"presences\"\n  config.session_column = :session_id\n  config.subject_column = :user_id      # or :member_id, :student_id\n  config.subject_class = \"User\"         # or \"Member\", \"Student\"\n\n  config.timeout = 90                   # seconds until offline\n  config.heartbeat_interval = 30\n\n  # Observability: when true (default) and the host app runs a supported APM\n  # agent (New Relic today), presence heartbeats are kept out of APM as their\n  # own transactions. Heartbeats are the highest-volume, least-interesting cable\n  # action, so reporting each one inflates throughput and flatters average\n  # response time without adding signal. Failures are still reported. Set false\n  # to report heartbeats like any other action.\n  config.ignore_heartbeat_apm = true\n\n  # Optional: custom subject data in presence hash. NOTE: with the roster\n  # enabled, these fields are broadcast to every member of the org — see\n  # Security.\n  config.subject_data_proc = ->(user) {\n    { id: user.id, name: user.name, avatar: user.avatar_url }\n  }\n\n  # Live presence roster (see \"Live Presence Roster\"). Set roster_org to\n  # enable; roster_members is optional (defaults to org.<subjects>).\n  config.roster_org = ->(user) { user.account }\n  config.roster_members = ->(org) { org.users.active }\n\n  # Optional: skip presence registration for specific connections while\n  # keeping them subscribed for broadcasts. Return truthy to suppress. The\n  # subscriber still streams from where_is_waldo:subject:<id> and receives\n  # WhereIsWaldo.broadcast_to messages — they just don't count as present.\n  # Typical use: support-user impersonation tabs shouldn't light up as the\n  # impersonated user in teammates' rosters.\n  config.suppress_presence_proc = ->(connection) {\n    connection.request.session[:su_user].present?\n  }\n\n  # Redis adapter\n  # config.redis_client = Redis.new(url: ENV[\"REDIS_URL\"])\nend\n```\n\n### Querying Presence\n\n```ruby\n# Get online subjects from any AR scope\nWhereIsWaldo.online(org.users)\nWhereIsWaldo.online(User.where(role: \"admin\"))\nWhereIsWaldo.online(classroom.students)\n\n# Get just IDs\nWhereIsWaldo.online_ids(org.users)\n\n# Check specific subject\nWhereIsWaldo.subject_online?(user.id)\n\n# Get all sessions for a subject\nWhereIsWaldo.sessions_for_subject(user.id)\n# => [{ session_id: \"...\", tab_visible: true, subject_active: false, ... }]\n```\n\n### Broadcasting\n\n```ruby\n# To any AR scope\nWhereIsWaldo.broadcast_to(org.users, :notification, { message: \"Hi\" })\nWhereIsWaldo.broadcast_to(org.users.admin, :alert, { level: \"warning\" })\n\n# Only to online subjects\nWhereIsWaldo.broadcast_to_online(org.users, :update, { data: \"...\" })\n\n# To a single subject (all their sessions)\nWhereIsWaldo.broadcast_to(user, :force_logout, {})\n\n# To a specific session\nWhereIsWaldo.broadcast_to_session(session_id, :warning, { message: \"...\" })\n```\n\n### Live Presence Roster (\"who's around in my org\")\n\nA ready-made way to show live presence awareness across an org/account — who's\nhere right now, on what device, and how active. It is built as **data, not UI**:\nthe server keeps the client's roster in sync and you render whatever component\nyou like.\n\n**Delivery is a per-account strategy** so you can trade latency × cost ×\nvisibility-enforcement to fit your app. The server picks the mode and the client\nadapts automatically (no client mode config). Modes (default `:poll`):\n\n| Mode | Latency | Visibility | Cost/transition |\n|------|---------|------------|-----------------|\n| `:poll` (default) | ~poll interval | server-side query — **any** rule | flat (1 cached query/poll) |\n| `:nudge` | near-instant | server-side query — **any** rule | O(1) content-free trigger + filtered poll |\n| `:fanout` | instant | server-side per-viewer — **any** rule (incl. asymmetric) | O(audience) |\n| `:broadcast` | instant | **none** (everyone in account sees everyone) | O(1) |\n\n`:poll` sends a full **snapshot** on connect, then the client polls and the\nserver replies with a server-*filtered* diff (baseline cached per session,\nTTL'd for auto-resync) — so arbitrary/asymmetric visibility \"just works\" via\n`roster_visible_to`. `:nudge` is `:poll` plus a content-free \"re-poll\"\nbroadcast on each transition, so clients refresh near-instantly instead of\nwaiting for the next interval — same airtight server-side filtering, just lower\nlatency; the nudge carries no identity/state (only \"activity happened\").\n`:fanout` pushes instantly to a **per-viewer** stream: on a transition the\nsubject's delta goes to every viewer in its directional `roster_viewers_of`\n(the inverse of `roster_visible_to`), so even *asymmetric* visibility\n(manager-sees-report-but-not-vice-versa) is exact — at O(audience) broadcasts\nper transition. `:broadcast` instead streams one shared account stream and\npushes deltas instantly, with **no** visibility filtering (open-visibility\naccounts only); pair it with the client `filter` option below for *cosmetic*\nper-viewer hiding.\n\n```jsx\n// :broadcast + cosmetic client filter (NOT a security boundary — the full\n// roster still reaches the client; use a server-side mode to truly enforce).\nconst { online } = usePresenceRoster({ filter: (m) => canISee(m.id) });\n```\n\n#### Which mode should I use?\n\nTwo questions decide it:\n\n**1. Is the client *always* interested, or only *sometimes*?**\n- **Sometimes** — presence is ambient (an avatar stack / sidebar you glance at; the component mounts only when relevant). Use the **pull family** (`:poll` / `:nudge`): the client fetches only while something's mounted and cares, and the server is idle at rest.\n- **Always** — presence *is* the live experience (chat, collaborative cursors). Use the **push family** (`:fanout` / `:broadcast`): the server streams every change as it happens.\n\n**2. Then the within-family choice:**\n- `:poll` vs `:nudge` → **latency.** Both pull, both filtered. `:poll` is interval-fresh; `:nudge` is near-instant (a content-free \"changed\" ping triggers an immediate poll).\n- `:fanout` vs `:broadcast` → **visibility.** Both push. `:fanout` keeps per-viewer server-side filtering; `:broadcast` uses one shared stream with no filtering (open-visibility accounts only).\n\nTwo things people get subtly wrong:\n- **Filtering is not fanout-only.** `:poll`, `:nudge`, *and* `:fanout` all enforce visibility server-side. **`:broadcast` is the only unfiltered mode.**\n- **`:fanout` ships the data; `:nudge` ships a \"time to poll\" signal.** Both give instant, filtered updates — `:fanout` spends server fan-out (O(audience) data pushes), `:nudge` spends client polls (O(1) ping + a poll per interested viewer). Same result, different cost.\n\nSee `docs/PRESENCE_ROSTER_PLAN.md` for the full mode matrix and tradeoffs.\n\n**Per-device, multi-session.** A subject's state is aggregated across *all*\ntheir live sessions (multiple browser tabs, mobile, etc.):\n\n```\n{ id: 7, status: \"active\", devices: { web: \"idle\", mobile: \"active\" },\n  last_activity: \"2026-07-27T15:23:04Z\" }\n```\n\n- `status` — highest activity across devices (the \"active anywhere?\" answer):\n  - `active` — a live session is visible/foreground **and** working\n  - `idle` — a live session is visible/foreground but not actively using\n  - `background` — only backgrounded/hidden sessions are live\n  - `offline` — no live sessions\n- `devices[platform]` — that platform's own status (answers \"active on\n  **mobile**?\" vs. \"active at all?\").\n- `last_activity` — ISO8601 timestamp of the most recent input across **all**\n  the subject's live sessions (`null` when offline). The roster only\n  broadcasts on `status` transitions, so subtract against your own clock to\n  derive seconds-idle and apply client-side thresholds without a server\n  `timeout` change: `(Date.now() - new Date(m.last_activity)) / 1000`.\n\n#### Configure\n\n```ruby\nWhereIsWaldo.configure do |config|\n  # The org/account a subject belongs to. Required to enable the roster.\n  config.roster_org = ->(subject) { subject.account }\n\n  # What a VIEWER may see (:poll/:nudge/:fanout). Any visibility rule,\n  # server-enforced. Defaults to the viewer's whole org when unset.\n  config.roster_visible_to = ->(viewer) { viewer.visible_users }\n\n  # Who may see a SUBJECT — the inverse of roster_visible_to. Only needed\n  # for :fanout (enables instant push under asymmetric visibility).\n  config.roster_viewers_of = ->(subject) { subject.visible_to_users }\n\n  # Delivery mode, per account. Symbol or a callable resolving account -> mode.\n  # MUST be a function of the account (uniform for all its members). Default :poll.\n  config.roster_mode = ->(account) { account.everyone_admin? ? :broadcast : :poll }\n\n  # :broadcast only — the member list for the shared snapshot. Defaults to\n  # org.public_send(<subjects>) inferred from subject_class (e.g. :users).\n  config.roster_members = ->(org) { org.users.active }\n\n  # Tuning (:poll/:nudge)\n  config.roster_poll_interval = 15  # seconds\n  config.roster_cache_ttl     = 90  # seconds; > poll gap → auto-resync\nend\n```\n\nThe `RosterChannel` is provided by the gem; no app code is needed beyond config.\n\n#### Consume it (React — data hook, bring your own UI)\n\n```jsx\nimport { usePresenceRoster, presenceColor, memberLabel } from '@byscott-io/where-is-waldo';\n\nfunction TeamPresence() {\n  const { online, members, onlineCount, byId } = usePresenceRoster();\n\n  // `members` is always the full, live set (snapshot seeded, deltas patched).\n  return (\n    <ul>\n      {online.map((m) => (\n        <li key={m.id}>\n          <span style={{ color: presenceColor(m.status) }}>●</span>\n          {memberLabel(m)} — {m.status}\n          {m.devices.mobile && ' 📱'}\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\nThe hook and its reducer (`../core/rosterStore`) are pure — **no DOM** — so a\nReact Native app reuses the exact same data logic and only swaps the view.\n\n#### Query presence server-side\n\n```ruby\nWhereIsWaldo.roster_snapshot(org)            # full roster + per-device state\nWhereIsWaldo.roster_state_for(user.id)       # => { status:, devices: }\nWhereIsWaldo.presence_on(user.id, :mobile)   # => \"idle\" (per-device)\n```\n\n#### Reporting presence from mobile\n\nMobile is \"logged in\" purely by connecting with `metadata: { platform: \"mobile\" }`\nand sending the same heartbeat shape as the web client. The heartbeat/activity\nstate machine lives in a DOM-free core, `createPresenceReporter`, that the web\n`usePresence` hook wraps — a React Native app reuses the **same core** and only\nswaps the sensors: map app foreground/background to `setVisible`, and touches to\n`reportActivity`. No server-side changes; `platform` is read from the metadata\n(defaults to `\"web\"`).\n\n```jsx\n// React Native reporter — same core, native sensors.\nimport { useEffect, useRef } from 'react';\nimport { AppState, PanResponder } from 'react-native';\nimport { createPresenceReporter, configureCable } from '@byscott-io/where-is-waldo';\n\nconfigureCable({ url: WS_URL, getToken: () => token });\n\nexport function usePresenceNative() {\n  const reporterRef = useRef(null);\n\n  useEffect(() => {\n    const reporter = createPresenceReporter({ metadata: { platform: 'mobile' } });\n    reporterRef.current = reporter;\n    reporter.start();\n    reporter.setVisible(AppState.currentState === 'active');\n\n    const sub = AppState.addEventListener('change', (s) =>\n      reporter.setVisible(s === 'active'),\n    );\n    return () => {\n      sub.remove();\n      reporter.stop();\n    };\n  }, []);\n\n  // Feed touches as activity (attach these handlers to your root view).\n  const pan = useRef(\n    PanResponder.create({\n      onStartShouldSetPanResponderCapture: () => {\n        reporterRef.current?.reportActivity();\n        return false; // observe only; don't capture the gesture\n      },\n    }),\n  ).current;\n\n  return pan.panHandlers;\n}\n```\n\n`createPresenceReporter` is pure JS (no DOM), and `@rails/actioncable` works on\nReact Native with the built-in `WebSocket` — so the reporter, the roster hook\n(`usePresenceRoster`), and the roster reducer all run unchanged on mobile; only\nthe sensors and the view are platform-specific.\n\n### Client Event Subscriptions\n\nComponents subscribe to a raw event **type** with the `useWaldoEvent` hook,\nreceive the payload, and decide for themselves whether it's relevant. The\nsubscription auto-unsubscribes on unmount — there is no central handler\nregistry. Adding a new event = a server `broadcast_to` + a `useWaldoEvent`\ncall in whatever component cares.\n\n```jsx\nimport { useWaldoEvent } from '@byscott-io/where-is-waldo';\n\nfunction ChatRoom({ roomId }) {\n  // Single type — filter the payload yourself\n  useWaldoEvent('chat_message', (data) => {\n    if (data.room_id === roomId) addMessage(data);\n  });\n\n  // Many types at once. useWaldoEvent is a pure subscription — to coalesce a\n  // burst into one call, compose a debounce at the call site (e.g. corebyscott's\n  // useDebouncedCallback): const refetch = useDebouncedCallback(loadAll, 250);\n  useWaldoEvent(['notification', 'data_refresh'], refetch);\n\n  return null;\n}\n```\n\nFor a non-React / imperative context, `subscribeToEvent(type, cb)` returns an\nunsubscribe function:\n\n```js\nimport { subscribeToEvent } from '@byscott-io/where-is-waldo';\n\nconst unsubscribe = subscribeToEvent('chat_message', (data) => addMessage(data));\n// later: unsubscribe();\n```\n\n### React Hooks\n\n```jsx\nimport { usePresenceContext } from '@byscott-io/where-is-waldo';\n\nfunction StatusIndicator() {\n  const { connected, tabVisible, subjectActive } = usePresenceContext();\n\n  return <span>{connected ? 'Online' : 'Offline'}</span>;\n}\n```\n\n### Cleanup Job\n\n```ruby\n# For database adapter - schedule cleanup of stale records\n# config/initializers/sidekiq.rb\nSidekiq::Cron::Job.create(\n  name: 'Presence cleanup',\n  cron: '*/5 * * * *',\n  class: 'WhereIsWaldo::PresenceCleanupJob'\n)\n```\n\n### Version Management\n\n```bash\nrake version:show         # Show current version\nrake version:bump[0.1.0]  # Bump gem and npm together\n```\n\n## Security\n\nActionCable presence is only as safe as the connection auth around it. What the\ngem guarantees, and what your app must do:\n\n**Guaranteed by the gem**\n\n- **No client-chosen rooms.** `RosterChannel` derives the org from the\n  *authenticated connection* (`current_subject` → `roster_org`), never from a\n  client-supplied param — a user cannot subscribe to another org's roster.\n- **Self-scoped subject streams.** `PresenceChannel` streams only the\n  connection's own subject id, so targeted messages can't be eavesdropped.\n- **Unauthenticated connections are rejected** (`JwtConnection`).\n\n**Your app's responsibility**\n\n- **Identify the connection from a *verified* credential** (a signed JWT, as\n  `JwtConnection` does). Never trust a client-supplied `subject_id`. ⚠️ The\n  dummy app authenticates from a query param for tests only — do not copy that\n  into production.\n- **`subject_data_proc` fans out org-wide.** Every field it returns is\n  broadcast to all roster members. Include only what all members may see; keep\n  PII out unless intended.\n- **Visibility enforcement depends on the delivery mode.** `:poll`, `:nudge`,\n  and `:fanout` enforce server-side — `:poll`/`:nudge` by `presence_visible_\n  scope` (a `WHERE ... IN` clause), `:fanout` by `roster_viewers_of` — so\n  nothing an unauthorized member could read off the wire. `:broadcast` does\n  **no** filtering: it shares one account stream and pushes every member's\n  presence to everyone (`roster_members` only scopes the initial *snapshot\n  list*, not the live stream). The client `filter` option is **cosmetic** — the\n  full data still reaches the client — so it is not an access-control boundary.\n  Select `:broadcast` (± client filter) only for genuinely open-visibility\n  accounts; for any restricted visibility use a server-side mode (`:poll` is the\n  default). See `docs/PRESENCE_ROSTER_PLAN.md` for the full mode matrix.\n- **Token in the URL.** The JWT is passed as `?token=…`; use WSS only, keep\n  tokens short-lived, and avoid logging query strings. Set\n  `config.action_cable.allowed_request_origins` as defense-in-depth.\n- **Heartbeat/DoS.** Heartbeats are client-paced DB writes; roster broadcasts\n  are gated to transitions. For large or hostile deployments use the Redis\n  adapter and consider rate-limiting.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}