{"_id":"n-and-i","_rev":"8-7cb92e8964b75af3c01aea7100aaf849","name":"n-and-i","dist-tags":{"latest":"0.3.4"},"versions":{"0.0.1":{"name":"n-and-i","version":"0.0.1","author":{"url":"https://github.com/temich","name":"Artem Gurtovoi"},"license":"MIT","_id":"n-and-i@0.0.1","maintainers":[{"name":"agurtovoi","email":"tema.gurtovoy@gmail.com"}],"homepage":"https://github.com/temich/dpi#readme","bugs":{"url":"https://github.com/temich/dpi/issues"},"dist":{"shasum":"5bb374614eb657079066ce04f880e997b4b9752a","tarball":"https://registry.npmjs.org/n-and-i/-/n-and-i-0.0.1.tgz","fileCount":13,"integrity":"sha512-15q0xeIiFRDcdB8+uPezg1dFLNkS6NYs9mIrVl8lWRIMyjDw7xLh6+wJtYnoNMD76dvh4tYy7LwNCnI+tHQy8g==","signatures":[{"sig":"MEUCIAW/B+VfzUl9dxT9oBgCk66FlsXEctRAt9MoIo7gafUwAiEAzL2y21ZucM24XP9Pvxfot8U8UHjxzAjP4G4C51O1S2s=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":5852},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"5d745830e28f433524e2d48055efddbd8d016037","scripts":{"fmt":"oxfmt .","lint":"oxlint .","test":"node --test","build":"tsc -p tsconfig.build.json","check":"npm run typecheck && npm run lint && npm run fmt:check && npm test","prepare":"npm run build && (husky || true)","lint:fix":"oxlint --fix .","fmt:check":"oxfmt --check .","typecheck":"tsc --noEmit"},"_npmUser":{"name":"agurtovoi","email":"tema.gurtovoy@gmail.com"},"repository":{"url":"git+https://github.com/temich/dpi.git","type":"git"},"_npmVersion":"12.0.1","description":"Distributed peer indexing with Redis","directories":{},"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","oxfmt":"^0.65.0","oxlint":"^1.74.0","typescript":"^7.0.2","@types/node":"^26.1.1","@commitlint/cli":"^21.2.1","semantic-release":"^25.0.9","@semantic-release/git":"^11.0.1","@semantic-release/npm":"^13.1.2","@semantic-release/github":"^12.0.2","@commitlint/config-conventional":"^21.2.0","@semantic-release/commit-analyzer":"^13.0.1","@semantic-release/release-notes-generator":"^14.1.0","conventional-changelog-conventionalcommits":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/n-and-i_0.0.1_1788020706414_0.816243262794005","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"n-and-i","version":"0.1.0","author":{"url":"https://github.com/temich","name":"Artem Gurtovoi"},"license":"MIT","_id":"n-and-i@0.1.0","maintainers":[{"name":"agurtovoi","email":"tema.gurtovoy@gmail.com"}],"homepage":"https://github.com/temich/nandi#readme","bugs":{"url":"https://github.com/temich/nandi/issues"},"dist":{"shasum":"af1c719ecea8b1c052dae21156fae19fb6ee6bb8","tarball":"https://registry.npmjs.org/n-and-i/-/n-and-i-0.1.0.tgz","fileCount":28,"integrity":"sha512-khfuu92ZcRJTWxa53dVqWQsUm55yeBdpKYIVWymnFaQttZ+1a0kJpNkj5ht/S/cYyJe34tculpvowGtnRSDWPA==","signatures":[{"sig":"MEUCIDPv3XzbGG7nwRo6OIeIZMAEEJ/7Q6jDEnt1JnOMAAbTAiEA/M6R8GjVLRpo2cw+0UvW953NBwbJcVjodoGN+9K1Skk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/n-and-i@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":52394},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"b753c51a90a5abf1cbfe0d5b0d69296413eccd7d","scripts":{"fmt":"oxfmt .","lint":"oxlint .","test":"node --test","build":"tsc -p tsconfig.build.json","check":"npm run typecheck && npm run lint && npm run fmt:check && npm test","prepare":"npm run build && (husky || true)","lint:fix":"oxlint --fix .","fmt:check":"oxfmt --check .","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a908277-f615-4428-933a-1312a6c35a2e"}},"repository":{"url":"git+https://github.com/temich/nandi.git","type":"git"},"_npmVersion":"11.19.1","description":"Distributed peer indexing with Redis","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","oxfmt":"^0.65.0","redis":"^6.2.1","oxlint":"^1.80.0","ioredis":"^6.0.0","typescript":"^7.0.2","@types/node":"^26.4.0","@commitlint/cli":"^21.2.2","semantic-release":"^25.0.9","@semantic-release/npm":"^13.1.5","@semantic-release/github":"^12.0.9","@commitlint/config-conventional":"^21.2.2","@semantic-release/commit-analyzer":"^13.0.1","@semantic-release/release-notes-generator":"^14.1.1","conventional-changelog-conventionalcommits":"^9.3.1"},"_npmOperationalInternal":{"tmp":"tmp/n-and-i_0.1.0_1788029064009_0.35054099548413253","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"n-and-i","version":"0.2.0","author":{"url":"https://github.com/temich","name":"Artem Gurtovoi"},"license":"MIT","_id":"n-and-i@0.2.0","maintainers":[{"name":"agurtovoi","email":"tema.gurtovoy@gmail.com"}],"homepage":"https://github.com/temich/nandi#readme","bugs":{"url":"https://github.com/temich/nandi/issues"},"dist":{"shasum":"67c1627bb10d27193eb27c729726f242801289b4","tarball":"https://registry.npmjs.org/n-and-i/-/n-and-i-0.2.0.tgz","fileCount":28,"integrity":"sha512-YT9RhchWBPH1ZkMzaFpdikx6uc0LMT1KUb9POLV+MPplAw8nEptxIWBWbdzNva0PC8RWcYknJCzTATq68aGbtQ==","signatures":[{"sig":"MEUCIGqZXwhs35oX+IF97m9ZIAqK8g389N3enCbVSuGFV/6KAiEA9rZwTSep90rjL+oLrepRZ36cj73KEIYiBbZaw9agc6o=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/n-and-i@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":61113},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"9758da1c9ccdd5a315fd8c7288eadfd1cb9da31a","scripts":{"fmt":"oxfmt .","lint":"oxlint .","test":"node --test","build":"tsc -p tsconfig.build.json","check":"npm run typecheck && npm run lint && npm run fmt:check && npm test","prepare":"npm run build && (husky || true)","lint:fix":"oxlint --fix .","fmt:check":"oxfmt --check .","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a908277-f615-4428-933a-1312a6c35a2e"}},"repository":{"url":"git+https://github.com/temich/nandi.git","type":"git"},"_npmVersion":"11.19.1","description":"Distributed peer indexing with Redis","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","oxfmt":"^0.65.0","redis":"^6.2.1","oxlint":"^1.80.0","ioredis":"^6.0.0","typescript":"^7.0.2","@types/node":"^26.4.0","@commitlint/cli":"^21.2.2","semantic-release":"^25.0.9","@semantic-release/npm":"^13.1.5","@semantic-release/github":"^12.0.9","@commitlint/config-conventional":"^21.2.2","@semantic-release/commit-analyzer":"^13.0.1","@semantic-release/release-notes-generator":"^14.1.1","conventional-changelog-conventionalcommits":"^9.3.1"},"_npmOperationalInternal":{"tmp":"tmp/n-and-i_0.2.0_1788074880516_0.2699574776732636","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"n-and-i","version":"0.3.0","author":{"url":"https://github.com/temich","name":"Artem Gurtovoi"},"license":"MIT","_id":"n-and-i@0.3.0","maintainers":[{"name":"agurtovoi","email":"tema.gurtovoy@gmail.com"}],"homepage":"https://github.com/temich/nandi#readme","bugs":{"url":"https://github.com/temich/nandi/issues"},"dist":{"shasum":"2966bd40814a60b35b7d63d0e4c5e83f30e070c0","tarball":"https://registry.npmjs.org/n-and-i/-/n-and-i-0.3.0.tgz","fileCount":33,"integrity":"sha512-ws9LnVBf6TBisSnRl6O/iTNCtX5hLNXCH4fIyIevldwQGuufysBjPlFK42WK94qGX/HTuIDUkR4v0+mEfppzdQ==","signatures":[{"sig":"MEUCIQDOQgF5yzBAp7KdxEI++y/XSYiplr7hSaxfAkY7ZREjdAIgTSIr3oQRkdzhXKAheArrAp27x6Lways3xgqUOq26AWs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/n-and-i@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":78099},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"e25028e5174674be86a1f1a1776d0bdd9a1b550c","scripts":{"fmt":"oxfmt .","lint":"oxlint .","test":"node --test","build":"tsc -p tsconfig.build.json","check":"npm run typecheck && npm run lint && npm run fmt:check && npm test","prepare":"npm run build && (husky || true)","lint:fix":"oxlint --fix .","fmt:check":"oxfmt --check .","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a908277-f615-4428-933a-1312a6c35a2e"}},"repository":{"url":"git+https://github.com/temich/nandi.git","type":"git"},"_npmVersion":"11.19.1","description":"Distributed peer indexing with Redis","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","oxfmt":"^0.65.0","redis":"^6.2.1","oxlint":"^1.80.0","ioredis":"^6.0.0","typescript":"^7.0.2","@types/node":"^26.4.0","@commitlint/cli":"^21.2.2","semantic-release":"^25.0.9","@semantic-release/npm":"^13.1.5","@semantic-release/github":"^12.0.9","@commitlint/config-conventional":"^21.2.2","@semantic-release/commit-analyzer":"^13.0.1","@semantic-release/release-notes-generator":"^14.1.1","conventional-changelog-conventionalcommits":"^9.3.1"},"_npmOperationalInternal":{"tmp":"tmp/n-and-i_0.3.0_1788079981710_0.5837018848541946","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"n-and-i","version":"0.3.1","author":{"url":"https://github.com/temich","name":"Artem Gurtovoi"},"license":"MIT","_id":"n-and-i@0.3.1","maintainers":[{"name":"agurtovoi","email":"tema.gurtovoy@gmail.com"}],"homepage":"https://github.com/temich/nandi#readme","bugs":{"url":"https://github.com/temich/nandi/issues"},"dist":{"shasum":"3e82978a231ed78fa00d376769ca5a21b07b67a7","tarball":"https://registry.npmjs.org/n-and-i/-/n-and-i-0.3.1.tgz","fileCount":33,"integrity":"sha512-DMU4GTOVyG5lLhLRQb4D8D2Jsvp1ylZ71JwvkyYgTonw/ef+ybihPEvLtejbUXnz5PjPCm/EZgepfII+xwkPTQ==","signatures":[{"sig":"MEQCIBPidFMBCfhEd8HrjxyC5vAtCTCcgej/HFWAw4y4RGikAiAtJ5/tNHd5eywiimSNWpKzuRbH9FsHWbaSIf2EndE5EA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/n-and-i@0.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":78599},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"2a19a12ac05fa2145727ff5ffa34fd0688b6c564","scripts":{"fmt":"oxfmt .","lint":"oxlint .","test":"node --test","build":"tsc -p tsconfig.build.json","check":"npm run typecheck && npm run lint && npm run fmt:check && npm test","prepare":"npm run build && (husky || true)","lint:fix":"oxlint --fix .","fmt:check":"oxfmt --check .","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a908277-f615-4428-933a-1312a6c35a2e"}},"repository":{"url":"git+https://github.com/temich/nandi.git","type":"git"},"_npmVersion":"11.19.1","description":"Distributed peer indexing with Redis","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","oxfmt":"^0.65.0","redis":"^6.2.1","oxlint":"^1.80.0","ioredis":"^6.0.0","typescript":"^7.0.2","@types/node":"^26.4.0","@commitlint/cli":"^21.2.2","semantic-release":"^25.0.9","@semantic-release/npm":"^13.1.5","@semantic-release/github":"^12.0.9","@commitlint/config-conventional":"^21.2.2","@semantic-release/commit-analyzer":"^13.0.1","@semantic-release/release-notes-generator":"^14.1.1","conventional-changelog-conventionalcommits":"^9.3.1"},"_npmOperationalInternal":{"tmp":"tmp/n-and-i_0.3.1_1788278934978_0.04942397681808686","host":"s3://npm-registry-packages-npm-production"}},"0.3.2":{"name":"n-and-i","version":"0.3.2","author":{"url":"https://github.com/temich","name":"Artem Gurtovoi"},"license":"MIT","_id":"n-and-i@0.3.2","maintainers":[{"name":"agurtovoi","email":"tema.gurtovoy@gmail.com"}],"homepage":"https://github.com/temich/nandi#readme","bugs":{"url":"https://github.com/temich/nandi/issues"},"dist":{"shasum":"a6374d5ea47c9cd77305dc878bce769dcdd916eb","tarball":"https://registry.npmjs.org/n-and-i/-/n-and-i-0.3.2.tgz","fileCount":33,"integrity":"sha512-Odg7j5vkkIK5ygIsKIz/wZVi5irJ1EABeL5bTUsfCEkjOfQRe7Fyz0Tk2nc91JgCcpbod95Tk5d3tqB46lBfFw==","signatures":[{"sig":"MEQCIGtJlkSqg9wh66CQnZNjr43wA1VPPjxnrH/qYwjtH7YxAiBkSMC4Y2lTY07u8c8XTJBNGoNBXrXQA9y3Ca7RHgUzJQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/n-and-i@0.3.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":78605},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"aae649f731c52dd7e209433c06b5c78dc05a4b8a","scripts":{"fmt":"oxfmt .","lint":"oxlint .","test":"node --test","build":"tsc -p tsconfig.build.json","check":"npm run typecheck && npm run lint && npm run fmt:check && npm test","prepare":"npm run build && (husky || true)","lint:fix":"oxlint --fix .","fmt:check":"oxfmt --check .","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a908277-f615-4428-933a-1312a6c35a2e"}},"repository":{"url":"git+https://github.com/temich/nandi.git","type":"git"},"_npmVersion":"11.19.1","description":"Distributed peer indexing with Redis","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","oxfmt":"^0.65.0","redis":"^6.2.1","oxlint":"^1.80.0","ioredis":"^6.0.0","typescript":"^7.0.2","@types/node":"^26.4.0","@commitlint/cli":"^21.2.2","semantic-release":"^25.0.9","@semantic-release/npm":"^13.1.5","@semantic-release/github":"^12.0.9","@commitlint/config-conventional":"^21.2.2","@semantic-release/commit-analyzer":"^13.0.1","@semantic-release/release-notes-generator":"^14.1.1","conventional-changelog-conventionalcommits":"^9.3.1"},"_npmOperationalInternal":{"tmp":"tmp/n-and-i_0.3.2_1788342951911_0.8519618280149543","host":"s3://npm-registry-packages-npm-production"}},"0.3.3":{"name":"n-and-i","version":"0.3.3","author":{"url":"https://github.com/temich","name":"Artem Gurtovoi"},"license":"MIT","_id":"n-and-i@0.3.3","maintainers":[{"name":"agurtovoi","email":"tema.gurtovoy@gmail.com"}],"homepage":"https://github.com/temich/nandi#readme","bugs":{"url":"https://github.com/temich/nandi/issues"},"dist":{"shasum":"50900840ed6d8e19d60f7213e312e0a106468ec7","tarball":"https://registry.npmjs.org/n-and-i/-/n-and-i-0.3.3.tgz","fileCount":33,"integrity":"sha512-i2SC9MiEEqMydApRrA95MLU+LHjggm4Aohk1epTWV+vQs2drXkhZwKOykYkvF1N2b5txldDME9Ylly7bGBA5LQ==","signatures":[{"sig":"MEYCIQCFB7VeDyerOjeLJBqHFyawdfovhYDVCk7xApcYUjG4PAIhANAyA5GPOPiUB/Zirz33jnL6Npj4xZ/2zmDL+WC78Afv","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCICnvyIA8ybW06rS8cA5tiaj8EdL7EO+6IfGNiWyzx6pxAiAcb4sw6UZiUL8jGohny8+cEC9QsH+zUG0RUy5/C9kh8w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/n-and-i@0.3.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":80112},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"6e0fb9add026f16dbc7398df2d1b468c6308f00a","scripts":{"fmt":"oxfmt .","lint":"oxlint .","test":"node --test","build":"tsc -p tsconfig.build.json","check":"npm run typecheck && npm run lint && npm run fmt:check && npm test","prepare":"npm run build && (husky || true)","lint:fix":"oxlint --fix .","fmt:check":"oxfmt --check .","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a908277-f615-4428-933a-1312a6c35a2e"}},"repository":{"url":"git+https://github.com/temich/nandi.git","type":"git"},"_npmVersion":"11.19.1","description":"Distributed peer indexing with Redis","directories":{},"_nodeVersion":"24.20.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","oxfmt":"^0.66.0","redis":"^6.2.1","oxlint":"^1.80.0","ioredis":"^6.0.0","typescript":"^7.0.2","@types/node":"^26.4.0","@commitlint/cli":"^21.2.2","semantic-release":"^25.0.9","@semantic-release/npm":"^13.1.5","@semantic-release/github":"^12.0.9","@commitlint/config-conventional":"^21.2.2","@semantic-release/commit-analyzer":"^13.0.1","@semantic-release/release-notes-generator":"^14.1.1","conventional-changelog-conventionalcommits":"^9.3.1"},"_npmOperationalInternal":{"tmp":"tmp/n-and-i_0.3.3_1788802516344_0.9526181703177965","host":"s3://npm-registry-packages-npm-production"}},"0.3.4":{"_id":"n-and-i@0.3.4","bugs":{"url":"https://github.com/temich/nandi/issues"},"dist":{"shasum":"e0405989b4c4416ca1310105d0c776d6994401c8","tarball":"https://registry.npmjs.org/n-and-i/-/n-and-i-0.3.4.tgz","fileCount":33,"integrity":"sha512-4C3Y1gjJBsYvUCTUSgkll1zpStuRJF3gBgVYO5M3dFjfhETDtqPLKcC54uNGguuqPCQpkzBk0/+NU31ZeZjPCQ==","signatures":[{"sig":"MEYCIQC/Ntid4ro8YeK6NJ26kv+Gw7bJ4wryt34CaJuA1gbN8wIhAOsFAVCddeaTgCh6ExCrLVWgjn+OG7I3J9Yc5Yoobqit","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCn6UHITFu3iIRdbc9Lxj8kZqp65ULVmstwVZrG8SURUAIhAOvQhz3QgVRc11Q9INLg7FEUfHVZrNHSM1MMX9u2EcxC"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/n-and-i@0.3.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":80116},"main":"./dist/index.js","name":"n-and-i","type":"module","types":"./dist/index.d.ts","author":{"url":"https://github.com/temich","name":"Artem Gurtovoi"},"engines":{"node":">=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"c31c06f0e82e81a0dfb057618829e2696bc324eb","license":"MIT","scripts":{"fmt":"oxfmt .","lint":"oxlint .","test":"node --test","build":"tsc -p tsconfig.build.json","check":"npm run typecheck && npm run lint && npm run fmt:check && npm test","prepare":"npm run build && (husky || true)","lint:fix":"oxlint --fix .","fmt:check":"oxfmt --check .","typecheck":"tsc --noEmit"},"version":"0.3.4","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a908277-f615-4428-933a-1312a6c35a2e"}},"homepage":"https://github.com/temich/nandi#readme","repository":{"url":"git+https://github.com/temich/nandi.git","type":"git"},"_npmVersion":"11.19.1","description":"Distributed peer indexing with Redis","directories":{},"maintainers":[{"name":"agurtovoi","email":"tema.gurtovoy@gmail.com"}],"_nodeVersion":"24.20.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","oxfmt":"^0.66.0","redis":"^6.2.1","oxlint":"^1.80.0","ioredis":"^6.0.0","typescript":"^7.0.2","@types/node":"^26.4.0","@commitlint/cli":"^21.2.2","semantic-release":"^25.0.9","@semantic-release/npm":"^13.1.5","@semantic-release/github":"^12.0.9","@commitlint/config-conventional":"^21.2.2","@semantic-release/commit-analyzer":"^13.0.1","@semantic-release/release-notes-generator":"^14.1.1","conventional-changelog-conventionalcommits":"^9.3.1"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/n-and-i_0.3.4_1788874509567_0.3287922571193984"}}},"time":{"created":"2026-08-29T16:25:06.165Z","modified":"2026-09-08T13:35:10.013Z","0.0.1":"2026-08-29T16:25:06.548Z","0.1.0":"2026-08-29T18:44:24.142Z","0.2.0":"2026-08-30T07:28:00.678Z","0.3.0":"2026-08-30T08:53:01.861Z","0.3.1":"2026-09-01T16:08:55.133Z","0.3.2":"2026-09-02T09:55:52.041Z","0.3.3":"2026-09-07T17:35:16.425Z","0.3.4":"2026-09-08T13:35:09.669Z"},"bugs":{"url":"https://github.com/temich/nandi/issues"},"author":{"url":"https://github.com/temich","name":"Artem Gurtovoi"},"license":"MIT","homepage":"https://github.com/temich/nandi#readme","repository":{"url":"git+https://github.com/temich/nandi.git","type":"git"},"description":"Distributed peer indexing with Redis","maintainers":[{"name":"agurtovoi","email":"tema.gurtovoy@gmail.com"}],"readme":"# Distributed peer indexing\n\nWorkers that split a shared task source by `task.id % n === i` need two numbers.\n[Read more](https://temich.net/notes/peers/)\n\nThe library works them out for systems with dynamic topology, using Redis as the\ncoordinator.\n\n## Usage\n\n```sh\nnpm install n-and-i\n```\n\n```ts\nimport { discover } from 'n-and-i'\n\nfor await (const { i, n } of discover({ redis, name: 'mail-sender', interval: 30_000 })) {\n  await drain() // stop taking new work, finish or release what is in flight\n\n  if (i === null) continue // not registered — stay stopped\n\n  run(task => task.id % n === i)\n}\n```\n\nThe loop yields only when ownership changes, so its body is exactly the point at\nwhich to hand work over: stop consuming, drain, adopt the new pair, resume. A\nsettled group yields once and then stays quiet for as long as its membership\nholds.\n\nWhat it hands you is an exclusive lease. Two consecutive intervals have to agree\non a pair before it reaches the loop, and it then runs for `interval + gap`,\nrenewed by every registration that agrees again. Exclusive is meant literally:\nwhile you hold `i`, no other worker in the group holds it. When a registration\ndoes not arrive — the server is gone, slow, or answering nothing at all — the\nlease runs out and the loop hands you the idle pair instead. Nothing outside the\nworker revokes it, so there is never anything to wait for.\n\n### Options\n\n| Option     |          |                                                                                                             |\n| ---------- | -------- | ----------------------------------------------------------------------------------------------------------- |\n| `redis`    | required | An [ioredis](https://github.com/redis/ioredis) or [node-redis](https://github.com/redis/node-redis) client. |\n| `name`     | required | Worker group name, for example `mail-sender`.                                                               |\n| `interval` | required | Interval length in milliseconds.                                                                            |\n| `gap`      | `0.15`   | Timing slack, as a fraction of the interval.                                                                |\n| `prefix`   | `''`     | Prepended to the key, for namespacing.                                                                      |\n| `signal`   |          | An `AbortSignal`; aborting ends the loop, as `break` does.                                                  |\n| `console`  |          | Where to report what the loop is doing.                                                                     |\n\n### Choosing an interval\n\n> TL;DR: set it to 5 seconds.<br/>\n> After your first downtime read this section carefully.\n\nThe interval has to comfortably exceed a registration round trip and the jitter\naround it, and it also sets the pace of everything else. A worker owns nothing\nuntil two consecutive closed intervals have agreed on its pair, so a new one\nstarts consuming **no earlier than** two intervals after it comes up, and a\ndeparture costs the group about one interval of standing down.\n\nShort intervals react faster but spend a larger share of themselves in transit,\nand leave less room between workers. Long intervals are calmer but slower to\nreflect reality. Nothing below a second is sensible in production, though the\nlibrary enforces no floor — its own tests run at 300ms.\n\n### Shutting down\n\nAborting the signal yields the idle pair one last time before the loop finishes,\nso a worker stands down inside the loop body — the same place it already handles\nlosing its registration:\n\n```ts\nconst control = new AbortController()\nprocess.on('SIGTERM', () => control.abort())\n\nconst peers = discover({\n  redis,\n  name: 'mail-sender',\n  interval: 30_000,\n  signal: control.signal,\n})\n\nfor await (const { i, n } of peers) {\n  await drain()\n\n  if (i === null) continue\n\n  consume(task => task.id % n === i)\n}\n\n// the loop never ends while the worker still owns a slot\nawait redis.quit()\n```\n\nLeaving with `break` is immediate instead — a generator cannot yield once the\nconsumer has left — so draining from there is your own.\n\nEither way the worker stays counted in the interval it last registered in, so\nits slot goes unowned for up to one interval after it leaves, exactly as if it\nhad crashed.\n\n## How it works\n\nEach worker `INCR`s a key naming the current interval. The reply is its index\nfor that interval; the value the previous key closed on is the replica count.\nRedis holds nothing but counters — no membership list, no identities:\n\n```\n{mail-sender}:58391 = 3      <- closed, so its count is final\n{mail-sender}:58392 = 3      <- open, still accepting registrations\n```\n\nThe interval number comes from `TIME` inside the script, so every worker agrees\non it by construction and clock differences between workers do not come into it.\n\n**Both numbers come from the same closed interval.** A worker keeps the index it\nreceived last time in memory and pairs it with that interval's final count. The\nregistrants of `N-1` therefore hold exactly `0..n-1`, each once — a complete\npartition with no overlap. Pairing an index from the open interval with a count\nfrom the closed one is what would let a worker claim a slot in a group it was\nnot part of.\n\nThat also settles what happens to a worker that has just started: it has no\nindex from a closed interval, so it has nothing to pair, and it owns nothing\nuntil the interval it registered in has closed. The same check covers a worker\nthat stalled past an interval or restarted — the absence of a usable index is\nwhat makes it wait, and no identity or registry of members is needed to detect\nit.\n\n**And the pair has to hold for two intervals running.** Each worker adopts its\nnew pair when its own registration returns, not at a shared instant, so a pair\nthat has just changed is not yet the one the whole group is on. Publishing it\nstraight away would put two mappings live at once, and a task that changed hands\ncould be picked up twice. So a worker owns a slot only when the interval before\nlast implied the same pair, and otherwise owns nothing until it does. Two\nanswers in a row are what issue the lease; a disagreement is what withholds it.\n\nThat is what makes the lease exclusive, and it needs no coordinator. At any\nmoment the group spans two consecutive intervals at most. Owning `i` through the later one\nmeans having held `i` through the earlier one too, and `INCR` hands out distinct\nindices inside an interval — so the two claimants are the same worker. What it\ncosts is a stand-down: any change to `n` changes every pair, so the whole group\nlets go for an interval and comes back together, on the same slots. Registration\ncarries on throughout, which is what brings the group back to the arrangement it\nhad.\n\n### Registration timing\n\nWorkers do not all register at the same moment. Each fires at `(i + 0.5) / n` of\nthe interval, worked out from the values its previous call returned. Ordering\nthen stops depending on network jitter, and the arrangement is\nself-reinforcing: a worker holding index 2 of 5 fires third and is handed index\n2 again.\n\nThe spread covers `interval - 2 * gap`, so nobody sits on a boundary where a\nlittle jitter would carry the registration into the neighbouring key. The same\n`gap` is how far past its slot a worker may drift before it stands down — one\nnumber, because the two have to agree: a worker that missed a registration is\nrunning on a mapping two intervals old, and it has to be gone before anyone\ntakes up a newer one. Standing down `gap` late always beats the `2 * gap` the\nband leaves for it, whatever the gap is set to.\n\nWidening it makes the scheme more forgiving of slow round trips and stalled\nevent loops, at the cost of bunching registrations toward the middle of the\ninterval. Narrowing it does the reverse; below about `0.125` a single failed\nregistration costs the worker a cycle, because the first retry no longer lands\ninside the slack. It has to sit between 0 and 0.5.\n\nA settled group keeps its assignment indefinitely. A join shifts only the\nindices after the insertion point; a departure shifts only those after the gap.\nTwo workers that do collide are handed different indices and separate on the\nnext interval.\n\nRegistration is independent of ownership: a worker holds its slot in the\ninterval throughout a stand-down, so the group comes back onto the indices it\nalready had rather than having to agree on an order again.\n\n## What this does not give you\n\n**Ownership is eventual.** The pair in force describes intervals that have\nalready closed, so membership changes take two to three of them to show up. This\nsuits groups that tolerate controlled rebalancing; it is not a substitute for a\nreal coordination protocol when ownership must change immediately and\nconsistently.\n\n**Rebalances cost an interval.** No two workers are ever handed the same index\nat the same moment, but the price is that a membership change stands the whole\ngroup down for about an interval before it comes back on the new `n`. Groups\nthat would rather keep serving through a rebalance, and fence the work\nthemselves, want a different scheme.\n\n**The guarantee ends at the loop body.** What the group never does is hand the\nsame index to two workers at once. What happens to work already in flight is\nyours: a consumer that keeps going after it has been handed a new pair, instead\nof draining first, can still finish a task another worker has since started.\n\n**A worker that registers and then dies leaves its slot unowned** until the next\ninterval, because the count is a snapshot of who _was_ present. No membership\nscheme avoids this; only a shorter interval narrows it.\n\n**Registration failures never reach the loop.** A blip is retried within the\ninterval and costs nothing. Drifting past the slot it was due in by more than\nthe gap expires the lease and hands the loop the idle pair rather than raising —\nwhether the registration failed, or hung and never came back at all. A worker\nRedis has stopped counting stops consuming, and comes back the long way round,\nexactly as a restart would. Silent is not the same as invisible: both show up on\nthe `console` if you pass one, the first as `registration failed` and the second\nas `lease expired`.\n\n## Redis Cluster\n\nKeys are written as `{name}:N`. The braces are a hash tag, so every interval key\nof a group lands in one slot and the two keys the script touches are never\ncross-slot.\n\n## Diagnostics\n\nThe library writes nowhere of its own accord. Pass a `console` and it reports\nwhat it is doing to that instead — anything with `trace`, `debug`, `info`,\n`warn` and `error`, each taking a message and an attributes object. The global\n`console` fits as it is:\n\n```ts\ndiscover({ redis, name: 'mail-sender', interval: 30_000, console })\n```\n\nMessages are constants and every value travels in the attributes, the group\n`name` included, so lines group by message whatever the backend does with the\nrest.\n\n| level   | message                       | attributes                                     |\n| ------- | ----------------------------- | ---------------------------------------------- |\n| `error` | `registration failed`         | `error`, `attempt`, `delay`                    |\n| `warn`  | `lease expired`               | `interval`, `after`                            |\n| `info`  | `pair disagreed`              | `i`, `n`, `pi`, `pn` — after a held pair split |\n| `info`  | `pair agreed`                 | `i`, `n` — the agreement that closes a split   |\n| `debug` | `script loaded`               | `sha`                                          |\n| `trace` | `discover started`            | `interval`, `gap`, `prefix`, `registry`        |\n| `trace` | `discover stopped`            | `reason`: `abort` or `closed`                  |\n| `trace` | `registration completed`      | `interval`, `index`, `replicas`, `skew`        |\n| `trace` | `lease granted`               | `i`, `n`                                       |\n| `trace` | `lease released`              | `i`, `n` — the pair given back                 |\n| `trace` | `no pair implied`             | `pi`, `pn` — what the interval before implied  |\n| `trace` | `next registration scheduled` | `delay`, `expires`                             |\n| `trace` | `pair handed to the loop`     | `i`, `n`                                       |\n\n## Development\n\nThe tests are integration tests: they need a Redis, and fail without one.\n\n```sh\ndocker run --rm -p 6379:6379 redis:8-alpine\n\nnpm install\nnpm run check   # typecheck + lint + format check + tests\nnpm run build   # emits dist/\n```\n\n`REDIS_URL` points them elsewhere (default `redis://localhost:6379`), and\n`TEST_INTERVAL` sets the interval they run at in milliseconds (default `300`).\nA slower machine wants a larger one — CI uses `600`.\n\n| script              | does                                   |\n| ------------------- | -------------------------------------- |\n| `npm run typecheck` | `tsc --noEmit`                         |\n| `npm run lint`      | `oxlint .` (`lint:fix` to autofix)     |\n| `npm run fmt`       | `oxfmt .` (`fmt:check` in CI)          |\n| `npm test`          | `node --test` over `src/**/*.test.ts`  |\n| `npm run build`     | `tsc -p tsconfig.build.json` → `dist/` |\n\n`husky` runs `npm run check` on pre-commit and lints the message against\n[conventional commits](https://www.conventionalcommits.org) on commit-msg.\n\n## Branches and releases\n\n- `dev` — the default branch. Changes land here through a pull request that\n  passes `check` and carries an approving review.\n- `release` — merging `dev` into it runs `semantic-release`, which derives the\n  version from the commit messages, tags it, publishes the package to npm, and\n  cuts a GitHub Release with the generated notes.\n\nCommit messages are the release input, so `commit-msg` lints them against\n[conventional commits](https://www.conventionalcommits.org). The version in this\nrepository's `package.json` is not bumped by the release — the git tags are the\nrecord, and `semantic-release` sets the published version at publish time.\n\nPublishing to npm authenticates over OIDC — npm trusted publishing, configured\nagainst this repository and `release.yml`. No `NPM_TOKEN` secret is involved:\nthe workflow requests `id-token: write` and npm (>= 11.5.1, which Node 24\nbundles) exchanges that for a short-lived credential, and stamps the release\nwith a provenance attestation.\n\nThe trust relationship itself is registry-side state, not repository state:\n\n```sh\nnpm trust list n-and-i\n```\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for the branch and release procedure,\nand [SECURITY.md](SECURITY.md) for reporting a vulnerability.\n\n## License\n\n[MIT](LICENSE) © Artem Gurtovoi\n","readmeFilename":"README.md"}