{"_id":"@aikofy/client-db","_rev":"17-654c4001d6fb1038e3ed4fcfd749b1a3","name":"@aikofy/client-db","dist-tags":{"latest":"2.2.0"},"versions":{"0.1.0":{"name":"@aikofy/client-db","version":"0.1.0","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@0.1.0","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"99d68c1ce771a55eedc7771750a52c1407aeacf1","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-0.1.0.tgz","fileCount":9,"integrity":"sha512-5HSg/BgylqKHe3YdNrUoViV5gB1QynmekRT6jvFuP5rYW4s+w77ev7mALeW1bJgp8WVmE5txqfQXIHjtD2Pktw==","signatures":[{"sig":"MEQCIH1UTJ6r9A0U0RYS7Q/EwMwcgaIWAlwxlp3LF631r6+fAiB2ZaU8RcdnaOnh9xLjmr0d45Hiu9m6Z1OTAML+i+hDzw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":225018},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"22.20.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_0.1.0_1778496547263_0.25275122830175656","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@aikofy/client-db","version":"0.1.1","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@0.1.1","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"e546923f093188e70ce3c287ad87e32de4fad68a","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-0.1.1.tgz","fileCount":9,"integrity":"sha512-orbv2JN8eNAcgb1Yya3iJOCsArqQM+G0IEpr4pJgqDsLwA8iivPjvQJBRcW82sl2HwhVzy86RtHFjTxYOtCmzA==","signatures":[{"sig":"MEUCIBbWGdQwARqDMArs6Pu+etyNOUU9AujJVSs3IgEDRw4xAiEA4a67T8YvHoo2Nnq82OxKD2OhoRoyUcd4a7nr0duAqww=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":225018},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"4fa9d25ddf917cebd4daa495bfb8e10d09bfc102","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"22.20.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_0.1.1_1778517664026_0.5142556740391049","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@aikofy/client-db","version":"0.1.2","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@0.1.2","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"8ac6d659e46b946c6a8e7f1f1fcf72a861436025","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-0.1.2.tgz","fileCount":9,"integrity":"sha512-e3ZbScV7+FQ9Ibn2llOQWX0g77VoSnCti5/xYbAzqXiYDrfXMPF0i0jvcIJobJyBg7ae+6Ha0cv3y7dfiOWWIg==","signatures":[{"sig":"MEQCIC3ewkYH3oC9W/dwmAuVCkvHGE7k4LER5krspADlQ+iFAiBFgbzKgkLcWr4SLoqyXZqa3JLz6dNB1/pDtSwZt9Sp/A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":225018},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"5ddf18fbc1cfdefc3c67cabfde6143ed908d8cbe","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"22.20.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_0.1.2_1778518954112_0.10818093639553394","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@aikofy/client-db","version":"0.1.3","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@0.1.3","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"736160ec8e603afd11a14e7d2314532a784a90d6","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-0.1.3.tgz","fileCount":9,"integrity":"sha512-ZE5q2VzOk7ym7RCP0u/GyxhwNlMiR5UGLNGnhw0n/9vT8vv2cDVCwX+WY7xz31Q8EKytff4GyIPC3x7lQ2ROWQ==","signatures":[{"sig":"MEUCIDa02ZdS6j5xKaIJSd6Th7idQWCeSc+xLTe9ucNhFDClAiEA2jH92wSy17mVz3wtVi2AlDajY9wNUtZQ5req2fDG8U4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":225018},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"3c09b905860591d0f671e19a22388da2dbda1193","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"22.20.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_0.1.3_1778521606386_0.3399257446436681","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@aikofy/client-db","version":"0.1.4","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@0.1.4","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"6b3e0a316d2ad71ae3ec9b894caba3d5135f3617","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-0.1.4.tgz","fileCount":3,"integrity":"sha512-f6pI6LOWITkdCxr+jfMxXCT2LmyjCFf9Ah/ElcRhtszB/FSd5mOqUE7TYJytQ2dMb2t9FzEFV4tvjJinD/oH2Q==","signatures":[{"sig":"MEQCIDTCR4hHqVWS/Hcb8e6IRTIBg0dJtNT897aC4HFpotVxAiA9Y4sSFZI0g/d9ArVZ3zdmhKh4yzaufnTKloaNSKyf3w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":15220},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"3c09b905860591d0f671e19a22388da2dbda1193","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"22.20.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_0.1.4_1778523410410_0.911214743963134","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"name":"@aikofy/client-db","version":"0.1.5","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@0.1.5","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"d87c32b612b0847485bf15ba74f24ff82f808760","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-0.1.5.tgz","fileCount":9,"integrity":"sha512-uUAkn0ZhtQi9wXbCEBU1q+fV5wcoSx6CgyNWlo+kF8X/7hDuEiNu9OpT7kNaICSThWZ4gFKgrp6x4OvIuVbWqQ==","signatures":[{"sig":"MEQCIH33oZa5/sD8yRiYgf5+9rGpi4feT1juSYEfctAdst9dAiBzvKlX5DbzRKMQikByOwVzLdQv8qYdsO2qh2VAbDPmJg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":265455},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"e0498d0489eb662fd3fe2d00349469d88c5270ae","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"22.20.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_0.1.5_1778523697298_0.4665390629678634","host":"s3://npm-registry-packages-npm-production"}},"0.1.6":{"name":"@aikofy/client-db","version":"0.1.6","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@0.1.6","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"88b4192580ecf9469bcf6def418d6010d056e45a","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-0.1.6.tgz","fileCount":9,"integrity":"sha512-nxe1SHsXMTsJ7ay64Refy82a7Q0UQZova/mGQiVMlT3fiBcR+MelUAEDCgZHQpOXCsG+NKCp/ZyOblzdfObjxA==","signatures":[{"sig":"MEYCIQDvJDtOD6Crp56grZJIJi2Co2V7Ru1brTf0dcwi5L+GBwIhAOtLGvJKQXFIAB6BtTwJqa3CljDav1siXuzMRMYeAEuR","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":274181},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"e0498d0489eb662fd3fe2d00349469d88c5270ae","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"22.20.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_0.1.6_1778526244305_0.1410085265918224","host":"s3://npm-registry-packages-npm-production"}},"0.1.7":{"name":"@aikofy/client-db","version":"0.1.7","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@0.1.7","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"ea38fba48ba62d75a5fc10ea7fa72ea69f5492bc","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-0.1.7.tgz","fileCount":9,"integrity":"sha512-3DwOGzAlsgdvmzZOy+UXE0cyLOW+M7QxBN44UxYIJXz0fB6JDe4vum2tOLE4+ExFy8VbU57KM7CpPeY1UlPsJQ==","signatures":[{"sig":"MEYCIQDbANhPcPckidOpo+1mfuYOvLM2jZj37oW1NUGsNaA0ZQIhAP7mq2eGGtKxH3ceQs1jSfQvrX6RzsfCXPSJu2KFJGZ7","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":276499},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"e0498d0489eb662fd3fe2d00349469d88c5270ae","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"22.20.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_0.1.7_1778527065597_0.03700418470332245","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@aikofy/client-db","version":"1.0.0","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@1.0.0","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"5de95eb4abd6718a686aeb635abef4edfb9f43cb","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-1.0.0.tgz","fileCount":9,"integrity":"sha512-WzLJta3sw2Sa1AldxVenY4HRfdZEm+DJtziH2LfiLs0QcmpRgJWHqZ8efHW6Ajh9xrRPNhL1cQHef8xjz+vWYA==","signatures":[{"sig":"MEYCIQDRqvyZqcMnzWC7Nr+3q+ZY62vrVXkIk3hkgg5aIGirhQIhAKeNFrrng+MJ+nU4Z2Wj5XMB5bkd+9Y4VG7K4NoURWKN","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":300709},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"a9e00e4f7d5f2516e6d71d7cc3388fc9b7fe4876","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"22.20.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_1.0.0_1778529211705_0.1175737150082079","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@aikofy/client-db","version":"1.0.1","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@1.0.1","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"a980bfa644ed0dc34aaa519083c2568ddc0c0627","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-1.0.1.tgz","fileCount":9,"integrity":"sha512-/SaVh1CdPXs/nndcfXnigaMrJeg+e9s5rh+Lw3KFnLHfI/N+tQL9/P3M9y0J/64WmLqUg/eN7vRyXqy7SIgiew==","signatures":[{"sig":"MEUCIA3vr6t78gfWzw/6crJBkBttBQw79GY7f58HlxLJVW9nAiEAk2H5YoRapg3lqGaWflo6AJjQtxUzNRgEHPaPDsKd01U=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":317807},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"dedd4718a26a44e59082aa191e7398137c70b4da","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"22.20.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_1.0.1_1778530344492_0.30561472802566825","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@aikofy/client-db","version":"1.0.3","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@1.0.3","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"ca2e77b76527adf6b0876fb439f7358dfa7c3b0c","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-1.0.3.tgz","fileCount":9,"integrity":"sha512-waoIgnK32/0YrKokllLzCfL6ChH5XOtqqcCoAFMIw1gMncvtscYYmc2ORZBpkd0FCyvDkor2c0y86bLq4QGUww==","signatures":[{"sig":"MEUCIF5F4SGpg5wl+9K2SMSPcGyzIxebdfo3UAqzypy/f82eAiEAlTK+J5MYBK1LqZxuZDNioupF1mkfLywagTA8iBynrvM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":318931},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"4fd2f351b6a5e9773b5ba38a8bd9f4cbef1bc919","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"11.13.0","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"24.16.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_1.0.3_1779719102435_0.6830475519160499","host":"s3://npm-registry-packages-npm-production"}},"1.0.4":{"name":"@aikofy/client-db","version":"1.0.4","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":"Lwin Maung Maung","license":"MIT","_id":"@aikofy/client-db@1.0.4","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"80a2d28fa2f3f59950466c24a9449f5349a2ec67","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-1.0.4.tgz","fileCount":9,"integrity":"sha512-NFuoe5B7xspEYHBUOJTVJ/xcwx2nTOIhYWadT/MVOpBKKd8crzkldlco5PIkXQBhIFyXAC57DZiXFA3qlXu8qA==","signatures":[{"sig":"MEYCIQCzHxW2uDJMOyHTnX6jkX6tEbm9K5S9lXScOtuT9PwH2AIhAJiZJjyAg85sxS5xq202q44tBtVhBnz7/HVUyKBLExZn","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":321469},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","shasum":"80a2d28fa2f3f59950466c24a9449f5349a2ec67","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_integrity":"sha512-NFuoe5B7xspEYHBUOJTVJ/xcwx2nTOIhYWadT/MVOpBKKd8crzkldlco5PIkXQBhIFyXAC57DZiXFA3qlXu8qA==","_npmVersion":"10.8.3","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"24.3.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_1.0.4_1779720527070_0.7029334590078447","host":"s3://npm-registry-packages-npm-production"}},"1.0.6":{"name":"@aikofy/client-db","version":"1.0.6","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@1.0.6","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"dist":{"shasum":"02fc327a085bf99144523f3908d4bfae025c3b6b","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-1.0.6.tgz","fileCount":9,"integrity":"sha512-kwY6w/VCRH2S3b3Tc3A7S5I5Hr76b88SdxHqT4ng2saTk9rAsdm8+WaxwdAg46jM6RP+0szSarpvJ4+txAgFYA==","signatures":[{"sig":"MEYCIQDDVdxS0Mt3hVfvo5GP9y2VKyBhHxPgYAZyJ20Idu141wIhAOu6mveQLUqQTTsyFglG/STh+3VCaHHJggwSkSC9KTZy","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":327232},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"4fd2f351b6a5e9773b5ba38a8bd9f4cbef1bc919","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"11.13.0","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"24.16.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_1.0.6_1779723774336_0.9373206760661852","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@aikofy/client-db","version":"1.1.0","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@1.1.0","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"homepage":"https://github.com/aikofy/client-db#readme","bugs":{"url":"https://github.com/aikofy/client-db/issues"},"dist":{"shasum":"36af7a825c0b282d2e8f7544d78c63a0b0699c69","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-1.1.0.tgz","fileCount":9,"integrity":"sha512-7T3iaSZY8kxTXgtNRj9YJQX++8jWUK5xy1u9ObFnNKBfZnPoI8dLMySI/1oHkbr4kMV9MadzfpMZhBQdtkgSwg==","signatures":[{"sig":"MEUCIQC+d8B4PQw13wbg3saLZNWHXsHMTjJ/YLihv3AXIWYp9wIgC3MQd/CoQgW7ukDzXMPDGJZ7ViUgSOK8k1dpo/YD0b4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":458811},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"e2971098cd00091ceb1b2a708fa31241846beb28","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"repository":{"url":"git+https://github.com/aikofy/client-db.git","type":"git"},"_npmVersion":"11.13.0","description":"IndexedDB sync wrapper with HLC, conflict resolution, and WebRTC gossip sync","directories":{},"_nodeVersion":"24.16.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_1.1.0_1780216812495_0.21555987206295324","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@aikofy/client-db","version":"2.0.0","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt","rpc","consumer-client","p2p"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@2.0.0","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"homepage":"https://github.com/aikofy/client-db#readme","bugs":{"url":"https://github.com/aikofy/client-db/issues"},"dist":{"shasum":"5c24409ec156829be93f6d5abc4c0fc263fbc447","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-2.0.0.tgz","fileCount":17,"integrity":"sha512-YDnucbhnHQh5wrFsu+2ovvDe/yWgPWFETJoV7jzKtzmCRwd2M4EU3X+P6aIM5GGifLT8xLMX8HqdzOGWphLqdg==","signatures":[{"sig":"MEUCIQCnd2PntsbfkSUV+RYYYvPmIsdkxTzjxSW+WWziom0xfQIgGK2QTa248mxQfnwAdzzoomFm+Wo5sfhfksRM682JUtQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":827361},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./consumer":{"types":"./dist/consumer.d.ts","import":"./dist/consumer.js","require":"./dist/consumer.cjs"}},"gitHead":"3d01012d98a2923b4924f80a91d7c64bf7dba5fd","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"repository":{"url":"git+https://github.com/aikofy/client-db.git","type":"git"},"_npmVersion":"11.13.0","description":"Offline-first, peer-to-peer IndexedDB sync (HLC, CRDT conflict resolution, WebRTC gossip) with a Consumer Client RPC layer for thin clients that call functions instead of replicating","directories":{},"_nodeVersion":"24.16.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_2.0.0_1780334126108_0.24647530842936627","host":"s3://npm-registry-packages-npm-production"}},"2.1.0":{"name":"@aikofy/client-db","version":"2.1.0","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt","rpc","consumer-client","p2p"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db@2.1.0","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"homepage":"https://github.com/aikofy/client-db#readme","bugs":{"url":"https://github.com/aikofy/client-db/issues"},"dist":{"shasum":"fe10d0edc1730395748f2b6840d2ce6c95cc3673","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-2.1.0.tgz","fileCount":17,"integrity":"sha512-F4iSz/jlJyiqSyu2DaKe4Bmzf3lhb4BJgJYEWupyNp6jKK6cFgYwtvasuVcx5X/TpdJmEYOXZ1v/V9DZU5qfHA==","signatures":[{"sig":"MEUCIDetaLeHE7LaALtn4uTA8f6x0wjfUGSLamjkNTk2/0GPAiEA5/kKFn8gZG0/eYFHuAM05Q+mYLh6/eH6tztiFY7pekw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":922354},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./consumer":{"types":"./dist/consumer.d.ts","import":"./dist/consumer.js","require":"./dist/consumer.cjs"}},"gitHead":"0a2ed39dbe0ff4dc43f915c3c04cc8c2cacc2ca7","scripts":{"dev":"bunx tsup --watch","test":"bun run vitest run","build":"bunx tsup","typecheck":"bunx tsc --noEmit","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"repository":{"url":"git+https://github.com/aikofy/client-db.git","type":"git"},"_npmVersion":"11.13.0","description":"Offline-first, peer-to-peer IndexedDB sync (HLC, CRDT conflict resolution, WebRTC gossip) with a Consumer Client RPC layer for thin clients that call functions instead of replicating","directories":{},"_nodeVersion":"24.16.0","dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^10.0.0","fake-indexeddb":"^6.0.0","@vitest/coverage-v8":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db_2.1.0_1781274112869_0.7353232368286515","host":"s3://npm-registry-packages-npm-production"}},"2.2.0":{"name":"@aikofy/client-db","version":"2.2.0","description":"Offline-first, peer-to-peer IndexedDB sync (HLC, CRDT conflict resolution, WebRTC gossip) with a Consumer Client RPC layer for thin clients that call functions instead of replicating","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./consumer":{"types":"./dist/consumer.d.ts","import":"./dist/consumer.js","require":"./dist/consumer.cjs"}},"scripts":{"build":"bunx tsup","dev":"bunx tsup --watch","test":"bun run vitest run","test:watch":"bun run vitest","test:coverage":"bun run vitest run --coverage","typecheck":"bunx tsc --noEmit"},"dependencies":{"idb":"^8.0.0","uuid":"^10.0.0"},"devDependencies":{"@types/node":"^22.0.0","@types/uuid":"^10.0.0","@vitest/coverage-v8":"^2.0.0","fake-indexeddb":"^6.0.0","tsup":"^8.0.0","typescript":"^5.5.0","vitest":"^2.0.0"},"author":{"name":"Lwin Maung Maung"},"repository":{"type":"git","url":"git+https://github.com/aikofy/client-db.git"},"homepage":"https://github.com/aikofy/client-db#readme","bugs":{"url":"https://github.com/aikofy/client-db/issues"},"publishConfig":{"access":"public"},"keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt","rpc","consumer-client","p2p"],"license":"MIT","gitHead":"4648becc924de91a7745d6dfb917b607eaea6745","_id":"@aikofy/client-db@2.2.0","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-iRmcC/tRJ4IZ4sLZ1ztHrErQPR/rJoMTQVaSkBdMDxOZPYn/UFbdhO1ZMdJXePy1xE4lDWhAhpvAJQn5J39Fgw==","shasum":"230e4c717bc0af25f71e4eb18f6958a7a6dbb5cb","tarball":"https://registry.npmjs.org/@aikofy/client-db/-/client-db-2.2.0.tgz","fileCount":17,"unpackedSize":990375,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIADd9oZeBoG80bBjRNnNY23evhTbMFq4rir/kkf8vO0yAiEAjKpPac1Vqh1++wyWPa0T2I5ReXEOXkn9w8k3DKdHbiI="}]},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"directories":{},"maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/client-db_2.2.0_1781449198270_0.9579796981264408"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-11T10:49:07.184Z","modified":"2026-06-14T14:59:58.539Z","0.1.0":"2026-05-11T10:49:07.407Z","0.1.1":"2026-05-11T16:41:04.177Z","0.1.2":"2026-05-11T17:02:34.278Z","0.1.3":"2026-05-11T17:46:46.557Z","0.1.4":"2026-05-11T18:16:50.585Z","0.1.5":"2026-05-11T18:21:37.435Z","0.1.6":"2026-05-11T19:04:04.456Z","0.1.7":"2026-05-11T19:17:45.760Z","1.0.0":"2026-05-11T19:53:31.913Z","1.0.1":"2026-05-11T20:12:24.637Z","1.0.3":"2026-05-25T14:25:02.566Z","1.0.4":"2026-05-25T14:48:47.199Z","1.0.6":"2026-05-25T15:42:54.485Z","1.1.0":"2026-05-31T08:40:12.640Z","2.0.0":"2026-06-01T17:15:26.253Z","2.1.0":"2026-06-12T14:21:53.061Z","2.2.0":"2026-06-14T14:59:58.411Z"},"bugs":{"url":"https://github.com/aikofy/client-db/issues"},"author":{"name":"Lwin Maung Maung"},"license":"MIT","homepage":"https://github.com/aikofy/client-db#readme","keywords":["indexeddb","sync","webrtc","hlc","offline-first","crdt","rpc","consumer-client","p2p"],"repository":{"type":"git","url":"git+https://github.com/aikofy/client-db.git"},"description":"Offline-first, peer-to-peer IndexedDB sync (HLC, CRDT conflict resolution, WebRTC gossip) with a Consumer Client RPC layer for thin clients that call functions instead of replicating","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"readme":"# @aikofy/client-db\n\nA TypeScript-first, offline-ready, peer-to-peer syncing database for web browsers. Built on IndexedDB with a clean storage abstraction, Hybrid Logical Clock (HLC) ordering, pluggable conflict resolution, and WebRTC gossip sync — no server required for data replication.\n\n[![npm version](https://img.shields.io/npm/v/@aikofy/client-db)](https://www.npmjs.com/package/@aikofy/client-db)\n[![license](https://img.shields.io/npm/l/@aikofy/client-db)](./LICENSE)\n[![GitHub](https://img.shields.io/badge/GitHub-aikofy%2Fclient--db-181717?logo=github)](https://github.com/aikofy/client-db)\n\n---\n\n## Features\n\n- **Offline-first** — writes always succeed locally; sync happens automatically on reconnect\n- **Real-time peer sync** — every local write is pushed to all connected peers instantly over WebRTC data channels\n- **Room isolation** — peers only discover and sync with peers sharing the same DB name; one DB per user is safe\n- **Peer-to-peer** — gossip protocol over WebRTC, no central database server needed\n- **Hybrid Logical Clock (HLC)** — causal ordering of writes across nodes without relying on wall-clock agreement\n- **Smart bootstrap** — new peers auto-detect their state and pick the right sync strategy (snapshot, delta, or full merge)\n- **Pluggable conflict resolution** — Last-Write-Wins (default), First-Write-Wins, or a custom resolver per collection\n- **Change origin** — `onChange` callbacks receive `origin: 'local' | 'peer'` so you can distinguish your own writes from incoming sync changes\n- **Soft deletes** — tombstones preserve sync integrity; deleted records are never lost\n- **Sync-safe compaction** — `compact()` physically reclaims tombstone disk space in place once deletions age past the sync horizon, with no risk of resurrection. See [Reclaiming disk space](#reclaiming-disk-space-compaction).\n- **Delta sync** — only changes since last sync are exchanged, not full datasets\n- **Snapshot bootstrap** — new peers receive a full snapshot then switch to delta sync automatically\n- **Consumer Clients (RPC)** — thin clients that hold no data and call a Normal Client's\n  read/write/stream functions over WebRTC (auth, scopes, idempotent writes, streaming, failover);\n  ships as a separate ~17 KB entry point. See [Consumer Clients (RPC)](#consumer-clients-rpc).\n- **Fully typed** — strict TypeScript with generics; collection access is type-safe\n- **Framework-agnostic** — plain TypeScript, works in React, Vue, Svelte, or vanilla JS\n- **Tree-shakeable** — ESM + CJS dual build via tsup\n\n---\n\n## Installation\n\n```bash\nnpm install @aikofy/client-db\n# or\nbun add @aikofy/client-db\n# or\npnpm add @aikofy/client-db\n```\n\n**Peer requirements:** Modern browsers only (Chrome 89+, Firefox 86+, Safari 15+). No IE support.\n\n---\n\n## Quick Start\n\n```typescript\nimport { createDB } from '@aikofy/client-db';\n\nconst db = await createDB({\n  name: 'myapp',\n  version: 1,\n  collections: {\n    todos: {\n      indexes: ['status', ['userId', 'createdAt']],\n      conflictStrategy: 'lww', // last-write-wins (default)\n    },\n  },\n});\n\n// Write\nawait db.todos.put({ _id: 'todo-1', title: 'Buy milk', status: 'open' });\n\n// Read\nconst todo = await db.todos.get('todo-1');\n\n// Query\nconst openTodos = await db.todos.query({\n  where: { status: 'open' },\n  orderBy: '_updatedAt',\n  limit: 20,\n});\n\n// Delete (soft — tombstone, syncs to peers)\nawait db.todos.delete('todo-1');\n\n// React to local writes and incoming sync changes\nconst unsubscribe = db.todos.onChange((changes) => {\n  for (const change of changes) {\n    console.log(change.origin); // 'local' | 'peer'\n    console.log(change.operation); // 'put' | 'delete'\n  }\n});\n\n// Export / import snapshots\nconst snapshot = await db.export();\nawait db.import(snapshot);\n\n// Clean up\nawait db.close();\n```\n\n---\n\n## Sync (WebRTC P2P)\n\nPass a `sync` config to enable peer-to-peer sync via WebRTC. You need a signaling server (WebSocket) to help peers discover each other — only handshake metadata is exchanged over it, never your data.\n\nThe companion package [`@aikofy/client-db-sync`](https://www.npmjs.com/package/@aikofy/client-db-sync) is the ready-made signaling server for this library.\n\n### Room isolation\n\nPeers are automatically grouped by **DB name**. Only clients with the same `name` in their `createDB` config can discover and sync with each other. This makes it safe to use a userID as the DB name — each user's data stays completely isolated.\n\n### Development (no auth)\n\nStart the signaling server with `AUTH_DISABLED=true` and connect without a token:\n\n```typescript\nconst db = await createDB({\n  name: 'myapp',          // peers with name 'myapp' form one group\n  version: 1,\n  collections: {\n    notes: { indexes: ['authorId'] },\n  },\n  sync: {\n    signalingServer: 'ws://localhost:8080/signal',\n    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],\n  },\n});\n```\n\n### Production (JWT auth)\n\nWhen auth is enabled on the signaling server, your backend issues a token with `subject` set to the DB name (typically the userID). The library appends the room and token automatically:\n\n```typescript\n// 1. Your backend issues a token scoped to this user's DB name\nconst { token } = await fetch('https://signal.example.com/token', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    'x-admin-secret': process.env.SIGNAL_ADMIN_SECRET,\n  },\n  body: JSON.stringify({ ttl: '24h', subject: currentUser.id }),\n}).then(r => r.json());\n\n// 2. Use the userID as the DB name — the library ties the room to it automatically\nconst db = await createDB({\n  name: currentUser.id,   // DB name = room = token subject\n  version: 1,\n  collections: {\n    notes: { indexes: ['authorId'] },\n  },\n  sync: {\n    signalingServer: `wss://signal.example.com/signal?token=${token}`,\n    iceServers: [\n      { urls: 'stun:stun.l.google.com:19302' },\n      // Add TURN servers here for reliable NAT traversal in production\n    ],\n  },\n});\n```\n\nThe signaling server validates that the token's `subject` matches the DB name. A token for `user-123` can **only** join room `user-123` — knowing a userID is not enough to access another user's sync group.\n\n```typescript\nconsole.log(db.syncStatus); // 'online' | 'offline' | 'syncing'\n```\n\n### What happens after connection\n\nOnce connected, the library:\n\n1. Exchanges `peer-hello` messages with each connected peer (advertises current HLC watermark)\n2. Picks the right bootstrap strategy based on local state (see below)\n3. Pushes every local write to all connected peers **immediately** via RTCDataChannel (~20–100ms)\n4. Re-gossips every 30 seconds as a catch-up heartbeat\n5. On `window.online`, syncs with **all** connected peers to collect any offline writes\n\n> **Note:** The signaling server is only used for WebRTC handshake (offer/answer/ICE candidates). After connection, all data flows directly peer-to-peer.\n\n---\n\n## Consumer Clients (RPC)\n\n> **New in 2.0.0.** Authenticated, scope-authorized **reads, writes, and server-streaming** from a\n> Consumer against a Normal Client: JWS tokens verified locally with WebCrypto (ES256/RS256);\n> idempotent writes that replicate via the normal gossip pipeline; backpressure-aware, cancellable\n> streams; transparent **failover** to another Normal Client with **read-your-writes** preserved;\n> and **per-consumer rate limiting**, payload caps, an observability/audit hook, and\n> capability/version negotiation.\n>\n> **Requires a role-aware signaling server** to broker Consumer connections — see\n> [`docs/signaling-protocol.md`](./docs/signaling-protocol.md) (Normal-Client-only sync is\n> unaffected and needs no server change). Full design:\n> [`docs/consumer-client-plan.md`](./docs/consumer-client-plan.md),\n> [`docs/rpc-protocol.md`](./docs/rpc-protocol.md).\n\nBeyond peer-to-peer replication, the library supports a second client role so you can expose a\ncontrolled API surface instead of replicating the whole database:\n\n- **Normal Client** — the standard `createDB` client (full replica + gossip). It can *additionally*\n  act as an **RPC server**, exposing named read/write functions (\"handlers\") over WebRTC.\n- **Consumer Client** — a thin client (`@aikofy/client-db/consumer`, ~17 KB) that holds **no data**\n  and never gossips. It authenticates and *calls* a Normal Client's handlers. Think \"frontend\n  calling a backend,\" but the wire is a WebRTC data channel instead of HTTP.\n\nA Consumer can only ever receive what a handler returns — it can never pull the database.\n\n### Define handlers on a Normal Client\n\n```ts\nimport { createDB, RpcRouter, RpcError, createTokenVerifier } from '@aikofy/client-db';\n\nconst router = new RpcRouter()\n  .read('todos.listMine', {\n    scopes: ['todos:read'], // caller's token must carry these\n    handler: async (ctx) => {\n      // ctx.db is the FULL local replica — the handler decides what leaves the device.\n      const all = await ctx.db.query('todos');\n      return all.filter((t) => t.ownerId === ctx.consumer.id); // ctx.consumer = authenticated caller\n    },\n  })\n  .read('todos.get', {\n    scopes: ['todos:read'],\n    input: { parse: (v) => v as { id: string } }, // any zod-compatible `{ parse }`\n    handler: async (ctx, { id }) => {\n      const todo = await ctx.db.get('todos', id);\n      if (!todo) throw new RpcError('NOT_FOUND', 'no such todo');\n      return todo;\n    },\n  })\n  .write('todos.create', {\n    scopes: ['todos:write'],\n    handler: (ctx, params: { title: string }) =>\n      // ctx.idempotent dedupes retries (the Consumer auto-attaches a key for write methods).\n      ctx.idempotent(ctx.request.idempotencyKey, async () => {\n        const doc = await ctx.db.put('todos', { title: params.title, ownerId: ctx.consumer.id });\n        return { id: doc._id }; // the write replicates to other Normal Clients via gossip\n      }),\n  })\n  .stream('todos.export', {\n    scopes: ['todos:read'],\n    // An async generator. Each `yield` is one stream frame; backpressure is handled for you.\n    handler: async function* (ctx) {\n      const all = await ctx.db.query('todos');\n      for (let i = 0; i < all.length; i += 500) {\n        yield all.slice(i, i + 500).map((t) => ({ id: t._id }));\n      }\n    },\n  });\n\nconst db = await createDB({\n  name: currentUser.id,\n  version: 1,\n  collections: { todos: { indexes: ['ownerId'] } },\n  sync: { signalingServer, iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] },\n  rpc: {\n    router,\n    // Verify Consumer tokens locally (no per-request callout). Tokens are minted by your IdP;\n    // we only verify. `sub` becomes ctx.consumer.id; `scope`/`scopes` become ctx.consumer.scopes.\n    verifyToken: createTokenVerifier({\n      jwks: idpPublicJwks,           // your IdP's public keys\n      issuer: 'https://idp.example.com',\n      audience: 'client-db',\n    }),\n  },\n});\n```\n\nThe **handler body is the access-control boundary**: it reads the whole replica via `ctx.db` but\nreturns only what the authenticated `ctx.consumer` may see. Declared `scopes` are enforced before\nthe handler runs (missing scope → `PERMISSION_DENIED`). Omit `verifyToken` only in dev — without\nit every connection is trusted with no scopes.\n\n### Call from a Consumer Client\n\n```ts\nimport { ConsumerClient, RpcError } from '@aikofy/client-db/consumer';\n\nconst consumer = new ConsumerClient({\n  signalingServerUrl: 'wss://signal.example.com/signal',\n  room: targetUserId,                          // the Normal Clients' DB name\n  auth: { getToken: () => fetchAccessToken() }, // your IdP / backend\n});\n\nconst mine = await consumer.invoke('todos.listMine'); // connects + authenticates on first call\n\ntry {\n  const one = await consumer.invoke('todos.get', { id: 'abc' });\n} catch (e) {\n  if (e instanceof RpcError && e.status === 'NOT_FOUND') { /* handle */ }\n}\n\n// Writes work the same way; the SDK auto-attaches a stable idempotency key so a retry is\n// deduped (applied once) rather than duplicated.\nconst { id } = await consumer.invoke('todos.create', { title: 'Buy milk' });\n\n// Server-streaming: iterate chunks as they arrive. Breaking the loop (or an AbortSignal)\n// cancels the stream and stops the producer on the Normal Client.\nfor await (const batch of consumer.stream('todos.export')) {\n  render(batch);\n}\n\nconsumer.close();\n```\n\n`invoke()` resolves with the handler's return value or rejects with an `RpcError` carrying a\ngRPC-style `status` (`UNAUTHENTICATED`, `PERMISSION_DENIED`, `INVALID_ARGUMENT`, `NOT_FOUND`,\n`UNAVAILABLE`, …). The Consumer connects to one Normal Client chosen round-robin from the\nsignaling server's healthy list.\n\n### Failover & read-your-writes\n\nIf the serving Normal Client drops (or returns a retryable error), the Consumer transparently\nreconnects to the next candidate, re-authenticates, and replays the call:\n\n- **Reads** are replayed automatically. Each read carries the last write's HLC watermark as\n  `readAfter`, so a failed-over replica **waits until it has caught up** (read-your-writes) before\n  answering — or returns a retryable `UNAVAILABLE` so the client tries elsewhere.\n- **Writes are NOT replayed across nodes by default** — replaying a write on a different node can\n  duplicate it until the idempotency dedupe store is replicated (a follow-up). A dropped write\n  surfaces `UNAVAILABLE`; pass `{ idempotent: true }` to opt a write into failover-replay.\n\n```ts\nawait consumer.invoke('todos.listMine');                       // auto-replayed on failover\nawait consumer.invoke('todos.create', { title: 'x' });         // not replayed → surfaces error\nawait consumer.invoke('todos.create', { title: 'x' }, { idempotent: true }); // opt into replay\n```\n\n### Hardening & observability\n\nThe RPC server applies per-consumer **rate limiting** (token bucket → `RESOURCE_EXHAUSTED`),\n**payload caps** (`INVALID_ARGUMENT`), and a **concurrency cap** (`maxInflight`, default 64 —\nstops one consumer from holding hundreds of long-lived calls/streams open). Auth is bounded too:\ntokens over 8 KB are refused before any crypto work, and **5 failed auth attempts** lock the\nconnection out (`RESOURCE_EXHAUSTED`; reconnect to retry) so a single channel can't brute-force\ntokens or burn CPU on signature verifies. The server advertises its method catalog so the\nConsumer SDK **fails fast** (`NOT_FOUND`, no round-trip) on an unsupported method, and rejects a\n**newer protocol version** at auth. An `onCall` hook fires once per call for **metrics and audit**:\n\n```ts\nconst db = await createDB({\n  name, version, collections, sync,\n  rpc: {\n    router,\n    verifyToken,\n    onCall: (rec) => {\n      metrics.timing(`rpc.${rec.method}`, rec.durationMs, { status: rec.status });\n      if (rec.kind === 'write') {\n        // Audit trail keyed by the authenticated identity. Persist to a (replicated) collection\n        // for a durable, gossip-shared log.\n        void db.audit.put({ who: rec.identityId, method: rec.method, status: rec.status });\n      }\n    },\n  },\n});\n```\n\nLimits are configurable on the `rpc` option: `idempotencyTtlMs`, `readAfterTimeoutMs`, and\n`limits` (`maxPayloadBytes`, `defaultDeadlineMs`, `rateLimit.perMin`, `maxInflight`).\n\n---\n\n## Bootstrap Strategies\n\nWhen a client connects to peers for the first time in a session, it automatically picks the right strategy:\n\n### Case 1 — Pre-loaded snapshot (`initialSnapshot`)\n\nYou passed `initialSnapshot` to `sync`. The library awaits the promise (e.g. a Drive download), imports the snapshot into an empty DB, **then** connects to peers. After connecting it syncs bidirectionally with **all** peers from the snapshot's HLC watermark, catching any writes that happened since the snapshot was taken.\n\nIf the promise rejects or resolves to `null`, the library skips the import and falls through to Case 2.\n\n### Case 2 — Completely new client (empty DB)\n\nThe library waits briefly (300ms) to collect `peer-hello` responses from all initially-connected peers, then picks the **most up-to-date peer** (highest HLC) and requests their full snapshot. After applying it, it delta-syncs with all remaining peers to catch anything that peer didn't have.\n\n### Case 3 — Returning client (offline data)\n\nThe client has existing local data from a previous session. On reconnect, it syncs bidirectionally with **all** connected peers — not just a random subset — so every peer's offline writes are collected without loss.\n\n---\n\n## Cloud Snapshot Seeding\n\nUse `initialSnapshot` to seed a new browser from a cloud-stored snapshot (Google Drive, S3, etc.) before connecting to any peers. This avoids a slow full-snapshot bootstrap from a peer.\n\n```typescript\n// Download snapshot from your cloud storage\nasync function loadSnapshotFromDrive(): Promise<Snapshot | null> {\n  try {\n    const res = await fetch('https://storage.example.com/snapshots/user-123.json');\n    if (!res.ok) return null;\n    return res.json() as Promise<Snapshot>;\n  } catch {\n    return null;\n  }\n}\n\nconst db = await createDB({\n  name: currentUser.id,\n  version: 1,\n  collections: { todos: { indexes: ['status'] } },\n  sync: {\n    signalingServer: `wss://signal.example.com/signal?token=${token}`,\n    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],\n    // Peers are not contacted until this promise settles\n    initialSnapshot: loadSnapshotFromDrive(),\n  },\n});\n```\n\n**What happens:**\n\n| Scenario | Result |\n|----------|--------|\n| Drive download succeeds | Snapshot imported → peers connect → delta sync from snapshot watermark (fast) |\n| Drive download fails / returns `null` | No import → peers connect → full snapshot fetched from best peer (Case 2) |\n| DB already has local data | `initialSnapshot` is ignored (won't overwrite existing data) |\n\nTo keep the cloud snapshot fresh, call `db.export()` and upload the result periodically or on `window.beforeunload`.\n\n```typescript\n// Save snapshot to your cloud storage on page unload\nwindow.addEventListener('beforeunload', async () => {\n  const snapshot = await db.export();\n  navigator.sendBeacon(\n    'https://storage.example.com/snapshots/user-123.json',\n    JSON.stringify(snapshot),\n  );\n});\n```\n\n---\n\n## API Reference\n\n### `createDB(config)`\n\n```typescript\nconst db = await createDB(config: DBConfig);\n```\n\nReturns a `TypedDB<C>` — an object where each key of `collections` is a `CollectionProxy`, plus `export`, `import`, `compact`, `hardDelete`, `syncStatus`, and `close`.\n\n#### `DBConfig`\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `name` | `string` | IndexedDB database name. Also used as the sync room — peers with the same name sync together. |\n| `version` | `number` | Schema version — increment to trigger migrations |\n| `collections` | `Record<string, CollectionSchema>` | Collection definitions |\n| `sync` | `SyncConfig` (optional) | Enable WebRTC sync |\n\n#### `CollectionSchema`\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `indexes` | `(string \\| string[])[]` (optional) | Single-field or compound indexes |\n| `conflictStrategy` | `'lww' \\| 'first-write-wins' \\| resolver` (optional) | Defaults to `'lww'` |\n\n#### `SyncConfig`\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `signalingServer` | `string` | WebSocket URL. Append `?token=<jwt>` when auth is enabled. The `room` param is appended automatically from `DBConfig.name`. |\n| `iceServers` | `IceServer[]` | STUN/TURN servers for WebRTC |\n| `nodeId` | `string` (optional) | Override auto-generated node UUID |\n| `initialSnapshot` | `Snapshot \\| Promise<Snapshot \\| null \\| undefined>` (optional) | Seed the DB before connecting to peers. Peer connections are held until this settles. See [Cloud snapshot seeding](#cloud-snapshot-seeding). |\n| `changeLogTtlDays` | `number` (optional) | Days to keep change-log entries (and `_conflicts` audit records). Pruned on startup and every 24 h. Default `30`; `0` disables pruning. |\n| `maxClockDriftMs` | `number` (optional) | Reject remote docs whose HLC is further than this ahead of the local clock — bounds **clock poisoning** by a peer with a far-future clock. Default 24 h; `Number.POSITIVE_INFINITY` disables. |\n| `serveConsumers` | `boolean` (optional) | Whether this Normal Client accepts Consumer (`rpc`) connections. Default `true`. |\n| `autoCompact` | `boolean` (optional) | Physically reclaim tombstone disk space on the existing 24 h maintenance cycle (right after the change-log prune). Default `false` — opt in once you understand the [no-resurrection boundary](#reclaiming-disk-space-compaction). Requires `changeLogTtlDays > 0`. |\n\n---\n\n### `CollectionProxy<T>`\n\nAll collection operations are async and available on `db.<collectionName>`.\n\n#### `put(doc)`\n\n```typescript\nconst saved = await db.todos.put({ _id: 'abc', title: 'Hello', status: 'open' });\n// _id is auto-generated (UUID) if omitted\n```\n\nUpserts a record. Stamps `_rev` (HLC), `_updatedAt` (HLC), `_deleted: false`. Returns the full `Doc<T>`. The write is immediately pushed to all connected peers.\n\n#### `get(id)`\n\n```typescript\nconst todo = await db.todos.get('abc'); // Doc<T> | null\n```\n\n#### `query(options?)`\n\n```typescript\nconst results = await db.todos.query({\n  where: { status: 'open' },   // field equality filters\n  orderBy: '_updatedAt',        // any field\n  orderDir: 'desc',             // 'asc' (default) | 'desc'\n  limit: 10,\n  offset: 0,\n  includeDeleted: false,        // default false — excludes tombstones\n});\n```\n\n#### `delete(id)`\n\nSoft delete — sets `_deleted: true` with a new HLC revision. The record remains in IndexedDB (as a *tombstone*) and is synced as a deletion event to peers immediately. Tombstones are retained so the deletion can propagate; to physically reclaim their space once they age past the sync horizon, see [Reclaiming disk space (compaction)](#reclaiming-disk-space-compaction).\n\n#### `onChange(callback)`\n\n```typescript\nconst unsubscribe = db.todos.onChange((changes: ChangeEntry[]) => {\n  // fires on local writes AND incoming sync changes from peers\n  for (const change of changes) {\n    console.log(change.origin);    // 'local' | 'peer'\n    console.log(change.operation); // 'put' | 'delete'\n    console.log(change.id);        // document _id\n  }\n});\n\nunsubscribe(); // stop listening\n```\n\n---\n\n### `db.transaction(fn)` — atomic write batch\n\nGroup writes across collections into ONE IndexedDB transaction, so they commit\nall-or-nothing (docs, change-log entries, and the HLC watermark together):\n\n```typescript\nconst written = await db.transaction((tx) => {\n  tx.orders.put({ _id: orderId, status: 'placed' });\n  tx.orderItems.put({ orderId, sku: 'a', qty: 2 });\n  tx.carts.delete(cartId);\n});\n// written: the committed Docs in op order (tombstones included)\n```\n\nSemantics:\n\n- **All-or-nothing locally.** If any write fails, the transaction aborts — nothing is\n  persisted, no `onChange` fires, nothing is broadcast.\n- **Staging only.** The callback runs synchronously and stages writes; it cannot read.\n  Read what you need *before* the transaction, write inside. (This sidesteps the\n  IndexedDB auto-commit footgun where `await`-ing anything non-IndexedDB kills the tx.)\n- **Per-doc revisions.** Each op gets its own `_rev`, so per-doc LWW conflict resolution\n  is unchanged. Ops apply in order — a `delete` sees a `put` staged before it; deleting a\n  missing id is a no-op.\n- **Efficient fan-out.** Listeners get one `onChange` callback per collection, and peers\n  receive one gossip frame per collection, which they bulk-apply in a single transaction\n  per collection.\n- **Atomicity is local, not global.** Peers *receive* the batch together, but conflict\n  resolution stays per-doc: a concurrent remote edit can win LWW against one doc of your\n  batch and lose against another. Don't rely on cross-document invariants holding under\n  concurrent multi-peer writes; design docs so each is independently mergeable (or use a\n  custom `conflictStrategy`).\n\n---\n\n### System Fields\n\nEvery stored document has these read-only system fields:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `_id` | `string` | Primary key |\n| `_rev` | `HLCTimestamp` | HLC-based revision string |\n| `_deleted` | `boolean` | Tombstone flag |\n| `_updatedAt` | `HLCTimestamp` | Last write timestamp (HLC) |\n\n`HLCTimestamp` strings are lexicographically sortable — alphabetical order equals causal order.\n\n---\n\n### `ChangeEntry`\n\nPassed to `onChange` callbacks:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `id` | `string` | Document `_id` |\n| `collection` | `string` | Collection name |\n| `_rev` | `HLCTimestamp` | Revision at time of change |\n| `_updatedAt` | `HLCTimestamp` | Timestamp of change |\n| `operation` | `'put' \\| 'delete'` | Type of write |\n| `origin` | `'local' \\| 'peer'` | Who made the change |\n\n---\n\n### Conflict Resolution\n\n#### Built-in strategies\n\n```typescript\n// Last-Write-Wins: highest HLC timestamp wins (default)\nconflictStrategy: 'lww'\n\n// First-Write-Wins: lowest HLC timestamp wins\nconflictStrategy: 'first-write-wins'\n```\n\n#### Custom resolver\n\n```typescript\nconflictStrategy: (local, remote) => {\n  // Merge fields, pick a winner, or return any Doc shape\n  return { ...remote, score: local.score + remote.score };\n}\n```\n\nAll conflicts (both versions + resolved) are logged to an internal `_conflicts` collection for\naudit. Conflict records are **local-only diagnostics**: they never enter the change log (so they\nare not gossiped to peers) and are pruned on the same TTL as the change log\n(`sync.changeLogTtlDays`, default 30 days), so the audit trail can't grow without bound.\n\n---\n\n### Snapshot API\n\n```typescript\n// Export full snapshot (all collections + HLC watermark)\nconst snapshot = await db.export();\n\n// Import snapshot — use before connecting sync to seed a new client\nawait db.import(snapshot);\n```\n\nLarge snapshots (>256 KB) are automatically chunked over the WebRTC data channel.\n\n> **Heads-up:** `db.export()` / `db.import()` materialise the **whole** dataset in memory. For databases beyond a few hundred MB, use the streaming API below — it never holds more than one batch in memory.\n\n---\n\n### Streaming API (large databases)\n\n`export()`, `import()`, and an unbounded `query()` all load their full result set into the JS heap, which will exhaust memory on multi-GB stores. The streaming variants page the data and keep peak memory bounded to a single batch, so they scale to multi-GB databases.\n\n```typescript\n// Bounded-memory backup → NDJSON byte stream → upload (no full copy in RAM)\nimport { snapshotChunksToNdjsonStream, ndjsonStreamToSnapshotChunks } from '@aikofy/client-db';\n\nconst body = snapshotChunksToNdjsonStream(db.exportStream(/* batchSize = 1000 */));\nawait fetch(uploadUrl, { method: 'POST', body, duplex: 'half' });\n\n// Bounded-memory restore from a download stream\nconst res = await fetch(downloadUrl);\nawait db.importStream(ndjsonStreamToSnapshotChunks(res.body!));\n```\n\n```typescript\n// Iterate a large collection without materialising it (one page at a time)\nfor await (const todo of db.todos.scan({ where: { status: 'open' }, batchSize: 1000 })) {\n  process(todo); // pages by _id, yields in _id order\n}\n```\n\n| Method | Returns | Notes |\n|--------|---------|-------|\n| `db.exportStream(batchSize?)` | `AsyncGenerator<SnapshotChunk>` | header + keyset-paged batches; tombstones included |\n| `db.importStream(chunks)` | `Promise<void>` | consumes chunks; HLC watermark applied **after** all data lands |\n| `db.<collection>.scan(opts?)` | `AsyncGenerator<Doc>` | `where` + `includeDeleted`; `_id` order; no `orderBy` (use `query()` for sorted). When a `where` field is backed by a single-field index, scan pages **over the index** — O(matches) instead of O(collection) |\n| `snapshotChunksToNdjsonStream(chunks)` | `ReadableStream<Uint8Array>` | encode chunks as NDJSON, backpressure-aware |\n| `ndjsonStreamToSnapshotChunks(stream)` | `AsyncGenerator<SnapshotChunk>` | decode NDJSON bytes back to chunks |\n\nEach stage holds only one batch (`batchSize` docs) at a time, so memory stays flat regardless of database size.\n\n---\n\n### Reclaiming disk space (compaction)\n\n`delete()` is a **soft delete**: it stamps `_deleted: true` and a new HLC `_rev`, but the tombstone document stays physically in IndexedDB so the deletion can still propagate to peers. Over time tombstones (and pruned history) accumulate with no in-place way to shrink the DB. `compact()` physically removes tombstones **in place** — no close/reopen, while the DB stays open and serving:\n\n```typescript\nconst { tombstonesPurged, collections, bytesReclaimed } = await db.compact();\n// → purges tombstones whose deletion predates the horizon (default: changeLogTtlDays = 30 days)\n\n// Tune the horizon / scope:\nawait db.compact({ olderThanMs: 7 * 24 * 60 * 60 * 1000, collections: ['todos'], batchSize: 500 });\n\n// Also drop live docs you no longer want (replicated as ordinary deletes):\nawait db.compact({ predicate: (doc) => doc.archivedYearsAgo > 5 });\n```\n\n#### Why it's sync-safe (the no-resurrection boundary)\n\nA tombstone is only purged once its `_rev` is **older than the change-log TTL horizon**. Any peer that has not yet observed that deletion is, by definition, already past the change-log watermark — so on its next sync it is sent `needsFullSync` and re-bootstraps from a full snapshot. That snapshot contains **neither** the live doc **nor** its tombstone, so the record is correctly absent and never resurrects. **Within** the horizon, tombstones are retained so deletions keep propagating as deltas. Compaction piggybacks on the exact same TTL/watermark machinery as the change-log prune (`changeLogTtlDays`), so each node compacts its own replica independently — no cross-peer coordination needed.\n\nCompaction is **local-only** (never ticks the HLC, appends a change-log entry, or gossips), **idempotent**, and **crash-safe** (one bounded transaction per page; a partial run leaves every store and its indexes consistent). It pages by cursor, so it is safe on multi-GB stores.\n\nEnable it automatically on the 24 h maintenance cycle with `sync.autoCompact: true`, or call `db.compact()` yourself (e.g. on app idle).\n\n| Method | Returns | Notes |\n|--------|---------|-------|\n| `db.compact(options?)` | `Promise<CompactResult>` | Purge tombstones older than `options.olderThanMs` (default `changeLogTtlDays`, i.e. 30 days) across all collections or `options.collections`. `CompactResult` = `{ tombstonesPurged, collections, bytesReclaimed? }`. |\n| `db.hardDelete(collection, id)` | `Promise<boolean>` | ⚠️ **Sync-unsafe.** Physically remove ONE record immediately, without HLC tick / change-log entry / gossip. A peer still holding it can **resurrect** it via normal sync. Admin / non-synced contexts only — use `delete()` for replicated removal. |\n| `adapter.pruneTombstones(olderThanMs)` | `Promise<number>` | Low-level `IStorageAdapter` primitive `compact()` builds on (parallel to `pruneChanges`). Returns the count purged. |\n\n> **`hardDelete` caveat:** because it is never recorded or replicated, `hardDelete` is only correct when the record cannot come back from a peer (no sync, or a record this node alone owns). For everything synced, soft-`delete()` then let `compact()` reclaim the space.\n\n---\n\n### Advanced: Direct HLC Access\n\n```typescript\nimport { HLC, parseHLC } from '@aikofy/client-db';\n\nconst hlc = new HLC('my-node-id');\nconst ts = hlc.tick();          // generate next timestamp\nhlc.update(remoteTimestamp);    // advance clock from a remote event\nconst { physicalMs, counter, nodeId } = parseHLC(ts);\n```\n\n---\n\n### Advanced: Custom Storage Adapter\n\nThe `IStorageAdapter` interface is the abstraction layer. Implement it to swap in any backend (e.g., SQLite for React Native):\n\n```typescript\nimport type { IStorageAdapter } from '@aikofy/client-db';\n\nclass MySQLiteAdapter implements IStorageAdapter {\n  async put(collection, doc) { /* ... */ }\n  async get(collection, id) { /* ... */ }\n  async query(collection, options) { /* ... */ }\n  async delete(collection, id) { /* ... */ }\n  async bulkInsert(collection, docs) { /* ... */ }\n  async changes(since) { /* ... */ }\n  async pruneChanges(olderThanMs) { /* ... */ }\n  async pruneTombstones(olderThanMs) { /* physically remove aged tombstones; return count */ }\n  async getOldestChangesHlc() { /* ... */ }\n  async export() { /* ... */ }\n  async import(snapshot) { /* ... */ }\n  async collectionNames() { /* ... */ }\n  async close() { /* ... */ }\n}\n```\n\nAll sync, HLC, and conflict resolution logic is adapter-agnostic and reusable as-is.\n\n---\n\n## Caveats & Patterns\n\n### `createDB` is idempotent per `name`\n\nConcurrent or repeated `createDB` calls with the same `name` return the **same** in-flight promise. You can safely call it from a React `useEffect` (including under StrictMode, which double-fires effects in dev) without producing two transports.\n\nThe registry is cleared on `db.close()`, so close-then-reopen produces a fresh instance.\n\n> **Why this matters:** Without dedup, two `createDB` calls would each spin up a `WebRTCTransport` sharing the same persisted nodeId. The signaling server's \"one connection per nodeId per room\" rule then alternately kicks each socket with close code `1000 'replaced by new connection'` → reconnect → kick → loop.\n\n### Running multiple tabs of the same browser\n\nThe HLC `nodeId` (used for causal ordering) is persisted in IndexedDB and shared across same-origin tabs. The **signaling** `nodeId` should be unique per tab — otherwise the signaling server kicks the older tab when the newer one connects (same room, same nodeId).\n\nThe recommended pattern is to override `sync.nodeId` with a per-tab UUID stored in `sessionStorage`:\n\n```typescript\nfunction getSignalNodeId(): string {\n  const KEY = 'myapp-signal-node-id';\n  let id = sessionStorage.getItem(KEY);\n  if (!id) {\n    id = crypto.randomUUID();\n    sessionStorage.setItem(KEY, id);\n  }\n  return id;\n}\n\nconst db = await createDB({\n  name: currentUser.id,\n  version: 1,\n  collections,\n  sync: {\n    signalingServer: `wss://signal.example.com/signal?token=${token}`,\n    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],\n    nodeId: getSignalNodeId(),   // per-tab — distinct from the IDB-persisted HLC nodeId\n  },\n});\n```\n\n`sessionStorage` is per-tab (unlike `localStorage`), so each tab gets a unique UUID. The HLC nodeId stays in IndexedDB unchanged — only signaling identity is per-tab.\n\n### Peer changes and derived caches\n\nIf your app caches values derived from synced collections (e.g. account balances aggregated from transactions), **invalidate those caches inside your `onChange` peer handler** — local-write code paths typically invalidate caches inline, but peer writes land via gossip and bypass that code.\n\n```typescript\ndb.transactions.onChange((changes) => {\n  if (!changes.some(c => c.origin === 'peer')) return;\n  balanceCache.invalidateAll();   // peer txns shifted balances\n  refetchAccounts();              // now returns fresh computedBalance\n});\n```\n\n---\n\n## React Example\n\n```tsx\nimport { useEffect, useState } from 'react';\nimport { createDB } from '@aikofy/client-db';\nimport type { TypedDB } from '@aikofy/client-db';\n\nconst collections = {\n  todos: { indexes: ['status'], conflictStrategy: 'lww' as const },\n};\n\n// Fetch a short-lived token from your own backend (which calls @aikofy/client-db-sync's POST /token).\n// The token's subject must equal the DB name (userId).\n// In dev, skip this and use AUTH_DISABLED=true on the signaling server.\nasync function fetchSignalToken(userId: string): Promise<string> {\n  const res = await fetch('/api/signal-token', { method: 'POST' });\n  const { token } = await res.json() as { token: string };\n  return token;\n}\n\nlet dbInstance: TypedDB<typeof collections> | null = null;\n\nasync function getDB(userId: string) {\n  if (!dbInstance) {\n    const isDev = import.meta.env.DEV; // Vite / any bundler dev flag\n    const signalingServer = isDev\n      ? 'ws://localhost:8080/signal'\n      : `wss://signal.example.com/signal?token=${await fetchSignalToken(userId)}`;\n\n    dbInstance = await createDB({\n      name: userId,   // DB name = room — only this user's peers sync together\n      version: 1,\n      collections,\n      sync: {\n        signalingServer,\n        iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],\n      },\n    });\n  }\n  return dbInstance;\n}\n\nfunction useTodos(userId: string) {\n  const [todos, setTodos] = useState([]);\n\n  useEffect(() => {\n    let unsubscribe: (() => void) | undefined;\n\n    getDB(userId).then((db) => {\n      const load = () => db.todos.query({ orderBy: '_updatedAt', orderDir: 'desc' }).then(setTodos);\n      load();\n      unsubscribe = db.todos.onChange(() => load());\n    });\n\n    return () => unsubscribe?.();\n  }, [userId]);\n\n  return todos;\n}\n```\n\n---\n\n## Publishing Checklist (for maintainers)\n\nBefore publishing to npm:\n\n1. Bump `version` in `package.json` and add a `CHANGELOG.md` entry\n2. `bun run build` — verify `dist/` is clean (builds both the `.` and `./consumer` entries)\n3. `bun run test` — all tests pass\n4. `bun run typecheck` — no type errors\n5. `npm publish --access public` (or `bun publish`)\n\n---\n\n## Project Structure\n\n```\nsrc/\n  core/\n    types.ts          # IStorageAdapter interface + all shared types\n    hlc.ts            # Hybrid Logical Clock implementation\n    conflict.ts       # LWW, FWW, and custom conflict resolution\n    change-log.ts     # Change log helpers\n  storage/\n    schema.ts         # Collection schema builder\n    indexeddb.ts      # IndexedDB implementation of IStorageAdapter\n  sync/\n    webrtc-transport.ts  # WebRTC + WebSocket signaling (room-aware; isolates consumer 'rpc' channels)\n    gossip.ts            # Gossip sync protocol (K=3 fanout, 30s interval, real-time push)\n    snapshot.ts          # Snapshot export / import / chunking\n    backpressure.ts      # Data-channel backpressure (shared by gossip + RPC streaming)\n  rpc/                # Consumer RPC layer\n    protocol.ts          # Wire frames + status codes (docs/rpc-protocol.md)\n    router.ts            # RpcRouter — register read/write/stream handlers\n    server.ts            # RpcServer — dispatch handlers (runs on Normal Clients)\n    client.ts            # RpcClient — transport-agnostic caller (used by ConsumerClient)\n    auth.ts              # createTokenVerifier — WebCrypto JWS verification\n    idempotency.ts       # run-once dedupe for writes\n    middleware.ts        # TokenBucket rate limit, payload sizing, CallRecord\n    context.ts, errors.ts\n  db.ts               # createDB() factory + CollectionProxy (+ optional rpc server)\n  consumer.ts         # ConsumerClient — slim entry: @aikofy/client-db/consumer\n  index.ts            # Public exports\ndocs/                 # Consumer-client design + wire-protocol specs\nexamples/             # Normal-client / consumer-client usage examples\n```\n\n---\n\n## Dependencies\n\n| Package | Why |\n|---------|-----|\n| [`idb`](https://github.com/jakearchibald/idb) | Promise-based IndexedDB wrapper |\n| [`uuid`](https://github.com/uuidjs/uuid) | Node ID generation |\n\nNo paywalled or proprietary dependencies.\n\n---\n\n## Contributing\n\nContributions are welcome! The source lives on GitHub at [aikofy/client-db](https://github.com/aikofy/client-db). Please [open an issue](https://github.com/aikofy/client-db/issues) before submitting a PR for large changes.\n\n```bash\ngit clone https://github.com/aikofy/client-db.git\ncd client-db\nbun install\nbun run test        # run tests\nbun run test:watch  # watch mode\nbun run typecheck   # type check\nbun run build       # build dist/\n```\n\n---\n\n## License\n\nMIT © Aikofy\n","readmeFilename":"README.md"}