@noy-db/hub 0.2.0-pre.2 → 0.2.0-pre.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/dist/aggregate/index.cjs.map +1 -1
  2. package/dist/aggregate/index.js +2 -2
  3. package/dist/attestation/index.cjs.map +1 -1
  4. package/dist/attestation/index.d.cts +2 -2
  5. package/dist/attestation/index.d.ts +2 -2
  6. package/dist/attestation/index.js +6 -6
  7. package/dist/blobs/index.cjs.map +1 -1
  8. package/dist/blobs/index.d.cts +3 -3
  9. package/dist/blobs/index.d.ts +3 -3
  10. package/dist/blobs/index.js +5 -5
  11. package/dist/bundle/index.cjs +1245 -6
  12. package/dist/bundle/index.cjs.map +1 -1
  13. package/dist/bundle/index.d.cts +4 -4
  14. package/dist/bundle/index.d.ts +4 -4
  15. package/dist/bundle/index.js +10 -10
  16. package/dist/{chunk-EUYOGYGV.js → chunk-2EYC3WDT.js} +6 -6
  17. package/dist/{chunk-MUWOSVEP.js → chunk-2XLVPKXG.js} +2 -2
  18. package/dist/{chunk-NWZ3I6R6.js → chunk-4OQWR46B.js} +5 -5
  19. package/dist/{chunk-J4KLMEUL.js → chunk-4UBOTYP5.js} +2 -2
  20. package/dist/{chunk-UND4XIB6.js → chunk-4X2S7PBF.js} +3 -3
  21. package/dist/{chunk-VE6YVP32.js → chunk-5YHWBPOT.js} +2 -2
  22. package/dist/{chunk-7BUTTVMR.js → chunk-6S3LLAQ5.js} +2 -2
  23. package/dist/{chunk-7Z23ZFLV.js → chunk-74JEQFMT.js} +5 -5
  24. package/dist/{chunk-AHPFONIL.js → chunk-75QDHSE4.js} +5 -5
  25. package/dist/{chunk-3XHOCQK4.js → chunk-A6SWRXUQ.js} +2 -2
  26. package/dist/{chunk-PLI5TV7N.js → chunk-BFI3RS42.js} +2 -2
  27. package/dist/{chunk-CXSCDO5T.js → chunk-EMEX37ZN.js} +2 -2
  28. package/dist/{chunk-PEULZC6M.js → chunk-EPK6A3WJ.js} +8 -1
  29. package/dist/chunk-EPK6A3WJ.js.map +1 -0
  30. package/dist/{chunk-JYQTXEIO.js → chunk-FBMXWVGP.js} +5 -5
  31. package/dist/{chunk-QXQRKXCU.js → chunk-FCDO7UAO.js} +2 -2
  32. package/dist/{chunk-243PNUA6.js → chunk-FS7A4XNF.js} +3 -3
  33. package/dist/{chunk-YS3POABP.js → chunk-FXQYZNOW.js} +1 -1
  34. package/dist/chunk-FXQYZNOW.js.map +1 -0
  35. package/dist/{chunk-Y2RKOPNC.js → chunk-G7PAZ3TD.js} +4 -4
  36. package/dist/{chunk-QPEXPHJR.js → chunk-GAUBWHAF.js} +4 -4
  37. package/dist/{chunk-GIV6DWBG.js → chunk-GD3BGKAR.js} +2 -2
  38. package/dist/{chunk-TBKOGSYR.js → chunk-GDTCGIPX.js} +2 -2
  39. package/dist/{chunk-3Z2TPHC4.js → chunk-GVXBHCZ2.js} +8 -3
  40. package/dist/chunk-GVXBHCZ2.js.map +1 -0
  41. package/dist/{chunk-OVZDFEOR.js → chunk-HGZ7DC5H.js} +2 -2
  42. package/dist/{chunk-VPSUZLOJ.js → chunk-IS5HWQO7.js} +4 -4
  43. package/dist/{chunk-VPSUZLOJ.js.map → chunk-IS5HWQO7.js.map} +1 -1
  44. package/dist/{chunk-VK5EER6C.js → chunk-K5PVGKE4.js} +2 -2
  45. package/dist/{chunk-LRAZDV5X.js → chunk-KMI2NBBF.js} +6 -6
  46. package/dist/{chunk-VRBCTEKQ.js → chunk-KYKMKLJ6.js} +2 -2
  47. package/dist/{chunk-PFSNOPBQ.js → chunk-LOL725S4.js} +3 -3
  48. package/dist/{chunk-3Y53S2SA.js → chunk-LS3JLEIB.js} +4 -4
  49. package/dist/{chunk-XG3PTSCD.js → chunk-NCO2JGKK.js} +1 -1
  50. package/dist/chunk-NCO2JGKK.js.map +1 -0
  51. package/dist/{chunk-YTXSFG3C.js → chunk-NGSPBLLE.js} +2 -2
  52. package/dist/{chunk-FAQVNJD4.js → chunk-NSLTPGEN.js} +2 -2
  53. package/dist/{chunk-VCGTOS2A.js → chunk-P6256WTJ.js} +3 -3
  54. package/dist/{chunk-7Q5PLD5C.js → chunk-QAU5HM6Q.js} +3 -3
  55. package/dist/{chunk-HXJXPZRE.js → chunk-SAVQ6E2O.js} +2 -2
  56. package/dist/{chunk-E535SAN4.js → chunk-T6HQMVML.js} +1177 -51
  57. package/dist/chunk-T6HQMVML.js.map +1 -0
  58. package/dist/{chunk-Q6W2CMEJ.js → chunk-TLFUDXVV.js} +4 -4
  59. package/dist/{chunk-2PAQNPE3.js → chunk-UOF74WQY.js} +2 -2
  60. package/dist/{chunk-3QAKZ37R.js → chunk-UVPGJXVO.js} +5 -5
  61. package/dist/{chunk-7BRE6EUA.js → chunk-WRLHNG6H.js} +2 -2
  62. package/dist/{chunk-W3XXT26A.js → chunk-YDLAFP36.js} +43 -1
  63. package/dist/chunk-YDLAFP36.js.map +1 -0
  64. package/dist/{chunk-G6FRSBKK.js → chunk-YK72A4IT.js} +4 -4
  65. package/dist/{chunk-3S4BJX25.js → chunk-YL2DR3HY.js} +2 -2
  66. package/dist/{chunk-RTZVQAJ7.js → chunk-ZC2AAE6J.js} +2 -2
  67. package/dist/{chunk-4HIL6AHQ.js → chunk-ZUMGGHRB.js} +4 -4
  68. package/dist/consent/index.cjs.map +1 -1
  69. package/dist/consent/index.d.cts +3 -3
  70. package/dist/consent/index.d.ts +3 -3
  71. package/dist/consent/index.js +3 -3
  72. package/dist/{crypto-5ZDIY3NG.js → crypto-H2Y3DDFW.js} +3 -3
  73. package/dist/{delegation-QYXZW25W.js → delegation-QSC7G5QC.js} +5 -5
  74. package/dist/derivations/index.cjs.map +1 -1
  75. package/dist/derivations/index.d.cts +4 -4
  76. package/dist/derivations/index.d.ts +4 -4
  77. package/dist/derivations/index.js +4 -4
  78. package/dist/{dev-unlock-utkybTKb.d.ts → dev-unlock-Cf2B7Kih.d.ts} +1 -1
  79. package/dist/{dev-unlock-DQCNDfFp.d.cts → dev-unlock-De3mjQWv.d.cts} +1 -1
  80. package/dist/executor-BZKFZVRC.js +8 -0
  81. package/dist/executor-GFZFDQXV.js +8 -0
  82. package/dist/executor-KT2IOZVP.js +11 -0
  83. package/dist/{fanout-sidecar-VJ52RIEY.js → fanout-sidecar-NRBWSLRK.js} +2 -2
  84. package/dist/guards/index.cjs +7 -0
  85. package/dist/guards/index.cjs.map +1 -1
  86. package/dist/guards/index.d.cts +4 -4
  87. package/dist/guards/index.d.ts +4 -4
  88. package/dist/guards/index.js +4 -4
  89. package/dist/{hash-DcoYWfJ_.d.ts → hash-gVn_uKhp.d.ts} +1 -1
  90. package/dist/{hash-jDowCrK2.d.cts → hash-vBCB0-Ps.d.cts} +1 -1
  91. package/dist/history/index.cjs +1 -1
  92. package/dist/history/index.cjs.map +1 -1
  93. package/dist/history/index.d.cts +4 -4
  94. package/dist/history/index.d.ts +4 -4
  95. package/dist/history/index.js +6 -6
  96. package/dist/i18n/index.cjs.map +1 -1
  97. package/dist/i18n/index.d.cts +3 -3
  98. package/dist/i18n/index.d.ts +3 -3
  99. package/dist/i18n/index.js +7 -7
  100. package/dist/{index-BCKdioeh.d.ts → index-BF1B2HB9.d.ts} +25 -1
  101. package/dist/{index-BMjrzNZr.d.cts → index-DVkvrgpm.d.cts} +25 -1
  102. package/dist/index.cjs +1273 -27
  103. package/dist/index.cjs.map +1 -1
  104. package/dist/index.d.cts +33 -12
  105. package/dist/index.d.ts +33 -12
  106. package/dist/index.js +109 -42
  107. package/dist/index.js.map +1 -1
  108. package/dist/indexing/index.cjs.map +1 -1
  109. package/dist/indexing/index.js +2 -2
  110. package/dist/issue-BAJ7ZB4S.js +12 -0
  111. package/dist/{ledger-3IU5GMXA.js → ledger-WOEJUYTP.js} +6 -6
  112. package/dist/materialized-views/index.cjs.map +1 -1
  113. package/dist/materialized-views/index.d.cts +5 -5
  114. package/dist/materialized-views/index.d.ts +5 -5
  115. package/dist/materialized-views/index.js +6 -6
  116. package/dist/noydb-XNQSKXGO.js +34 -0
  117. package/dist/overlay-views/index.cjs.map +1 -1
  118. package/dist/overlay-views/index.d.cts +4 -4
  119. package/dist/overlay-views/index.d.ts +4 -4
  120. package/dist/overlay-views/index.js +4 -4
  121. package/dist/periods/index.cjs.map +1 -1
  122. package/dist/periods/index.d.cts +3 -3
  123. package/dist/periods/index.d.ts +3 -3
  124. package/dist/periods/index.js +6 -6
  125. package/dist/{public-envelope-U3CMEOMV.js → public-envelope-OHQ5UZFM.js} +4 -4
  126. package/dist/query/index.cjs.map +1 -1
  127. package/dist/query/index.d.cts +1 -1
  128. package/dist/query/index.d.ts +1 -1
  129. package/dist/query/index.js +3 -3
  130. package/dist/registry-2IEARCGT.js +7 -0
  131. package/dist/{registry-3ALP62P6.js → registry-CDHASH73.js} +3 -3
  132. package/dist/registry-EMGLZGR6.js +8 -0
  133. package/dist/registry-NQALYR77.js +8 -0
  134. package/dist/{revoke-KY2GB4KP.js → revoke-7JOVLZFD.js} +6 -6
  135. package/dist/session/index.cjs.map +1 -1
  136. package/dist/session/index.d.cts +4 -4
  137. package/dist/session/index.d.ts +4 -4
  138. package/dist/session/index.js +3 -3
  139. package/dist/shadow/index.cjs.map +1 -1
  140. package/dist/shadow/index.d.cts +3 -3
  141. package/dist/shadow/index.d.ts +3 -3
  142. package/dist/shadow/index.js +2 -2
  143. package/dist/{signer-GRI5TZKH.js → signer-M4K5HBLD.js} +5 -5
  144. package/dist/{stale-OTOF3FH7.js → stale-PAGCS4K5.js} +2 -2
  145. package/dist/store/index.cjs.map +1 -1
  146. package/dist/store/index.d.cts +3 -3
  147. package/dist/store/index.d.ts +3 -3
  148. package/dist/store/index.js +2 -2
  149. package/dist/sync/index.cjs.map +1 -1
  150. package/dist/sync/index.d.cts +2 -2
  151. package/dist/sync/index.d.ts +2 -2
  152. package/dist/sync/index.js +4 -4
  153. package/dist/team/index.cjs.map +1 -1
  154. package/dist/team/index.d.cts +3 -3
  155. package/dist/team/index.d.ts +3 -3
  156. package/dist/team/index.js +8 -8
  157. package/dist/tx/index.cjs +81 -1
  158. package/dist/tx/index.cjs.map +1 -1
  159. package/dist/tx/index.d.cts +4 -4
  160. package/dist/tx/index.d.ts +4 -4
  161. package/dist/tx/index.js +56 -3
  162. package/dist/tx/index.js.map +1 -1
  163. package/dist/{types-DJG8HG6F.d.cts → types-CSLcfytP.d.cts} +528 -5
  164. package/dist/{types-BoFFiskX.d.ts → types-D9eB0Rvh.d.ts} +528 -5
  165. package/dist/{ulid-C7ms9oli.d.cts → ulid-CG2YvAbg.d.cts} +1 -1
  166. package/dist/{ulid-BmBgooGm.d.ts → ulid-CiM2OAeM.d.ts} +1 -1
  167. package/dist/util/index.cjs.map +1 -1
  168. package/dist/util/index.js +1 -1
  169. package/dist/{with-derivation-BKXXa8Vt.d.ts → with-derivation-Bzpj6UTv.d.ts} +1 -1
  170. package/dist/{with-derivation-BjQ7q4NE.d.cts → with-derivation-DWajFh4K.d.cts} +1 -1
  171. package/dist/{with-guard-DQme5DKE.d.cts → with-guard-DF_Ul3DT.d.cts} +1 -1
  172. package/dist/{with-guard-C25yNjzd.d.ts → with-guard-DR7U-l4v.d.ts} +1 -1
  173. package/dist/{with-materialized-view-BbEPFIIJ.d.cts → with-materialized-view-_piodoIz.d.cts} +1 -1
  174. package/dist/{with-materialized-view-CqnRwI2S.d.ts → with-materialized-view-qtoJ3xKJ.d.ts} +1 -1
  175. package/dist/{with-overlayed-view-Ct1fSJt-.d.ts → with-overlayed-view-DFaRfgMr.d.ts} +1 -1
  176. package/dist/{with-overlayed-view-bwlmmFjx.d.cts → with-overlayed-view-DwzCKxn2.d.cts} +1 -1
  177. package/package.json +3 -3
  178. package/dist/chunk-3Z2TPHC4.js.map +0 -1
  179. package/dist/chunk-E535SAN4.js.map +0 -1
  180. package/dist/chunk-PEULZC6M.js.map +0 -1
  181. package/dist/chunk-W3XXT26A.js.map +0 -1
  182. package/dist/chunk-XG3PTSCD.js.map +0 -1
  183. package/dist/chunk-YS3POABP.js.map +0 -1
  184. package/dist/executor-AS2IDHKZ.js +0 -11
  185. package/dist/executor-HLXFXNFM.js +0 -8
  186. package/dist/executor-HN6YBHZ5.js +0 -8
  187. package/dist/issue-ORP37MVW.js +0 -12
  188. package/dist/noydb-5H3C24GG.js +0 -34
  189. package/dist/registry-7HE6VJGC.js +0 -8
  190. package/dist/registry-PSIPG2QR.js +0 -8
  191. package/dist/registry-RFGGMVNJ.js +0 -7
  192. /package/dist/{chunk-EUYOGYGV.js.map → chunk-2EYC3WDT.js.map} +0 -0
  193. /package/dist/{chunk-MUWOSVEP.js.map → chunk-2XLVPKXG.js.map} +0 -0
  194. /package/dist/{chunk-NWZ3I6R6.js.map → chunk-4OQWR46B.js.map} +0 -0
  195. /package/dist/{chunk-J4KLMEUL.js.map → chunk-4UBOTYP5.js.map} +0 -0
  196. /package/dist/{chunk-UND4XIB6.js.map → chunk-4X2S7PBF.js.map} +0 -0
  197. /package/dist/{chunk-VE6YVP32.js.map → chunk-5YHWBPOT.js.map} +0 -0
  198. /package/dist/{chunk-7BUTTVMR.js.map → chunk-6S3LLAQ5.js.map} +0 -0
  199. /package/dist/{chunk-7Z23ZFLV.js.map → chunk-74JEQFMT.js.map} +0 -0
  200. /package/dist/{chunk-AHPFONIL.js.map → chunk-75QDHSE4.js.map} +0 -0
  201. /package/dist/{chunk-3XHOCQK4.js.map → chunk-A6SWRXUQ.js.map} +0 -0
  202. /package/dist/{chunk-PLI5TV7N.js.map → chunk-BFI3RS42.js.map} +0 -0
  203. /package/dist/{chunk-CXSCDO5T.js.map → chunk-EMEX37ZN.js.map} +0 -0
  204. /package/dist/{chunk-JYQTXEIO.js.map → chunk-FBMXWVGP.js.map} +0 -0
  205. /package/dist/{chunk-QXQRKXCU.js.map → chunk-FCDO7UAO.js.map} +0 -0
  206. /package/dist/{chunk-243PNUA6.js.map → chunk-FS7A4XNF.js.map} +0 -0
  207. /package/dist/{chunk-Y2RKOPNC.js.map → chunk-G7PAZ3TD.js.map} +0 -0
  208. /package/dist/{chunk-QPEXPHJR.js.map → chunk-GAUBWHAF.js.map} +0 -0
  209. /package/dist/{chunk-GIV6DWBG.js.map → chunk-GD3BGKAR.js.map} +0 -0
  210. /package/dist/{chunk-TBKOGSYR.js.map → chunk-GDTCGIPX.js.map} +0 -0
  211. /package/dist/{chunk-OVZDFEOR.js.map → chunk-HGZ7DC5H.js.map} +0 -0
  212. /package/dist/{chunk-VK5EER6C.js.map → chunk-K5PVGKE4.js.map} +0 -0
  213. /package/dist/{chunk-LRAZDV5X.js.map → chunk-KMI2NBBF.js.map} +0 -0
  214. /package/dist/{chunk-VRBCTEKQ.js.map → chunk-KYKMKLJ6.js.map} +0 -0
  215. /package/dist/{chunk-PFSNOPBQ.js.map → chunk-LOL725S4.js.map} +0 -0
  216. /package/dist/{chunk-3Y53S2SA.js.map → chunk-LS3JLEIB.js.map} +0 -0
  217. /package/dist/{chunk-YTXSFG3C.js.map → chunk-NGSPBLLE.js.map} +0 -0
  218. /package/dist/{chunk-FAQVNJD4.js.map → chunk-NSLTPGEN.js.map} +0 -0
  219. /package/dist/{chunk-VCGTOS2A.js.map → chunk-P6256WTJ.js.map} +0 -0
  220. /package/dist/{chunk-7Q5PLD5C.js.map → chunk-QAU5HM6Q.js.map} +0 -0
  221. /package/dist/{chunk-HXJXPZRE.js.map → chunk-SAVQ6E2O.js.map} +0 -0
  222. /package/dist/{chunk-Q6W2CMEJ.js.map → chunk-TLFUDXVV.js.map} +0 -0
  223. /package/dist/{chunk-2PAQNPE3.js.map → chunk-UOF74WQY.js.map} +0 -0
  224. /package/dist/{chunk-3QAKZ37R.js.map → chunk-UVPGJXVO.js.map} +0 -0
  225. /package/dist/{chunk-7BRE6EUA.js.map → chunk-WRLHNG6H.js.map} +0 -0
  226. /package/dist/{chunk-G6FRSBKK.js.map → chunk-YK72A4IT.js.map} +0 -0
  227. /package/dist/{chunk-3S4BJX25.js.map → chunk-YL2DR3HY.js.map} +0 -0
  228. /package/dist/{chunk-RTZVQAJ7.js.map → chunk-ZC2AAE6J.js.map} +0 -0
  229. /package/dist/{chunk-4HIL6AHQ.js.map → chunk-ZUMGGHRB.js.map} +0 -0
  230. /package/dist/{crypto-5ZDIY3NG.js.map → crypto-H2Y3DDFW.js.map} +0 -0
  231. /package/dist/{delegation-QYXZW25W.js.map → delegation-QSC7G5QC.js.map} +0 -0
  232. /package/dist/{executor-AS2IDHKZ.js.map → executor-BZKFZVRC.js.map} +0 -0
  233. /package/dist/{executor-HLXFXNFM.js.map → executor-GFZFDQXV.js.map} +0 -0
  234. /package/dist/{executor-HN6YBHZ5.js.map → executor-KT2IOZVP.js.map} +0 -0
  235. /package/dist/{fanout-sidecar-VJ52RIEY.js.map → fanout-sidecar-NRBWSLRK.js.map} +0 -0
  236. /package/dist/{issue-ORP37MVW.js.map → issue-BAJ7ZB4S.js.map} +0 -0
  237. /package/dist/{ledger-3IU5GMXA.js.map → ledger-WOEJUYTP.js.map} +0 -0
  238. /package/dist/{noydb-5H3C24GG.js.map → noydb-XNQSKXGO.js.map} +0 -0
  239. /package/dist/{public-envelope-U3CMEOMV.js.map → public-envelope-OHQ5UZFM.js.map} +0 -0
  240. /package/dist/{registry-3ALP62P6.js.map → registry-2IEARCGT.js.map} +0 -0
  241. /package/dist/{registry-7HE6VJGC.js.map → registry-CDHASH73.js.map} +0 -0
  242. /package/dist/{registry-PSIPG2QR.js.map → registry-EMGLZGR6.js.map} +0 -0
  243. /package/dist/{registry-RFGGMVNJ.js.map → registry-NQALYR77.js.map} +0 -0
  244. /package/dist/{revoke-KY2GB4KP.js.map → revoke-7JOVLZFD.js.map} +0 -0
  245. /package/dist/{signer-GRI5TZKH.js.map → signer-M4K5HBLD.js.map} +0 -0
  246. /package/dist/{stale-OTOF3FH7.js.map → stale-PAGCS4K5.js.map} +0 -0
@@ -31,7 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
33
  // src/errors.ts
34
- var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, FieldFrozenError, InvariantError, AmendmentForbiddenError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, BundleVersionConflictError, ValidationError, SchemaValidationError, GroupCardinalityError, IndexRequiredError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, PartitionExtractionError, TransferSealError, AdoptionStateError, AttestationError, JoinTooLargeError, DanglingReferenceError, DerivationCycleError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError;
34
+ var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, FieldFrozenError, InvariantError, AmendmentForbiddenError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, BundleVersionConflictError, ValidationError, SchemaValidationError, SchemaUpdateError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, PartitionExtractionError, TransferSealError, AdoptionStateError, AttestationError, JoinTooLargeError, DanglingReferenceError, DerivationCycleError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError;
35
35
  var init_errors = __esm({
36
36
  "src/errors.ts"() {
37
37
  "use strict";
@@ -291,6 +291,30 @@ var init_errors = __esm({
291
291
  this.direction = direction;
292
292
  }
293
293
  };
294
+ SchemaUpdateError = class extends NoydbError {
295
+ constructor(code, message) {
296
+ super(code, message);
297
+ this.name = "SchemaUpdateError";
298
+ }
299
+ };
300
+ SchemaFenceError = class extends SchemaUpdateError {
301
+ constructor(message) {
302
+ super("SCHEMA_FENCE", message);
303
+ this.name = "SchemaFenceError";
304
+ }
305
+ };
306
+ MigrationRequiredError = class extends SchemaUpdateError {
307
+ constructor(message) {
308
+ super("MIGRATION_REQUIRED", message);
309
+ this.name = "MigrationRequiredError";
310
+ }
311
+ };
312
+ QuiesceTimeoutError = class extends SchemaUpdateError {
313
+ constructor(message) {
314
+ super("QUIESCE_TIMEOUT", message);
315
+ this.name = "QuiesceTimeoutError";
316
+ }
317
+ };
294
318
  GroupCardinalityError = class extends NoydbError {
295
319
  /** The field being grouped on. */
296
320
  field;
@@ -5578,7 +5602,10 @@ var init_transaction = __esm({
5578
5602
  "src/tx/transaction.ts"() {
5579
5603
  "use strict";
5580
5604
  init_errors();
5605
+ init_ulid();
5581
5606
  TxContext = class {
5607
+ /** Stable id for this transaction; shared by all writes it performs (#230). */
5608
+ txId = generateULID();
5582
5609
  /** @internal */
5583
5610
  _ops = [];
5584
5611
  /**
@@ -6617,6 +6644,11 @@ var init_collection = __esm({
6617
6644
  keyring;
6618
6645
  encrypted;
6619
6646
  emitter;
6647
+ writeQueue;
6648
+ schemaUpdateGate;
6649
+ schemaFence;
6650
+ writeHooks;
6651
+ activeTxId;
6620
6652
  getDEK;
6621
6653
  onDirty;
6622
6654
  historyConfig;
@@ -6891,6 +6923,11 @@ var init_collection = __esm({
6891
6923
  this.keyring = opts.keyring;
6892
6924
  this.encrypted = opts.encrypted;
6893
6925
  this.emitter = opts.emitter;
6926
+ this.writeQueue = opts.writeQueue;
6927
+ this.schemaUpdateGate = opts.schemaUpdateGate;
6928
+ this.schemaFence = opts.schemaFence;
6929
+ this.writeHooks = opts.writeHooks;
6930
+ this.activeTxId = opts.activeTxId;
6894
6931
  this.blobStrategy = opts.blobStrategy ?? NO_BLOBS;
6895
6932
  this.aggregateStrategy = opts.aggregateStrategy ?? NO_AGGREGATE;
6896
6933
  this.crdtStrategy = opts.crdtStrategy ?? NO_CRDT;
@@ -7116,7 +7153,8 @@ var init_collection = __esm({
7116
7153
  return this.syncStrategy.buildPresence(presenceOpts);
7117
7154
  }
7118
7155
  /**
7119
- * Create or update a record.
7156
+ * Create or update a record. Runs inside the hub's write-queue tracker
7157
+ * (#227) so `hub.writeQueue.pending` reflects this write.
7120
7158
  *
7121
7159
  * @param id Record identifier.
7122
7160
  * @param record The record body (validated by the collection's schema
@@ -7127,6 +7165,59 @@ var init_collection = __esm({
7127
7165
  * `entries.filter(e => e.reason?.startsWith('import:'))`.
7128
7166
  */
7129
7167
  async put(id, record, options) {
7168
+ await this.schemaUpdateGate?.assertWritable();
7169
+ await this.schemaFence?.assertWritable(this.name);
7170
+ let event;
7171
+ if (this.#hooksActive()) {
7172
+ const prior = await this.#priorForHook(id);
7173
+ event = {
7174
+ op: prior.record === null ? "create" : "update",
7175
+ vault: this.vault,
7176
+ collection: this.name,
7177
+ docId: id,
7178
+ before: prior.record,
7179
+ after: record,
7180
+ userId: this.keyring.userId,
7181
+ timestamp: Date.now(),
7182
+ txId: this.#txIdForHook(),
7183
+ baseVersion: prior.version,
7184
+ version: prior.version + 1
7185
+ };
7186
+ await this.writeHooks.runBefore(event);
7187
+ }
7188
+ if (this.writeQueue) await this.writeQueue.track(() => this.putInternal(id, record, options));
7189
+ else await this.putInternal(id, record, options);
7190
+ if (event) await this.writeHooks.runAfter(event);
7191
+ }
7192
+ /** @internal #230 — true when hooks should fire for this write (handlers exist, not re-entrant). */
7193
+ #hooksActive() {
7194
+ return this.writeHooks !== void 0 && this.writeHooks.hasHandlers && !this.writeHooks.suppressed;
7195
+ }
7196
+ /**
7197
+ * @internal #230/#228c — resolve the prior record for a hook's `before` and
7198
+ * its version. Critically, this uses the SAME basis `putInternal` writes from
7199
+ * (the in-memory cache in eager mode; lru-then-adapter in lazy) — NOT a fresh
7200
+ * store read — so `baseVersion`/`version` match the version actually written.
7201
+ * A separate store read would diverge once another tab has advanced the shared
7202
+ * store past this tab's cache, breaking #228c conflict detection.
7203
+ */
7204
+ async #priorForHook(id) {
7205
+ if (this.lazy && this.lru) {
7206
+ const cached2 = this.lru.get(id);
7207
+ if (cached2) return { record: cached2.record, version: cached2.version };
7208
+ const env = await this.adapter.get(this.vault, this.name, id);
7209
+ if (!env) return { record: null, version: 0 };
7210
+ return { record: await this.decryptRecord(env, { skipValidation: true }), version: env._v };
7211
+ }
7212
+ await this.ensureHydrated();
7213
+ const cached = this.cache.get(id);
7214
+ return cached ? { record: cached.record, version: cached.version } : { record: null, version: 0 };
7215
+ }
7216
+ #txIdForHook() {
7217
+ return this.activeTxId?.() ?? generateULID();
7218
+ }
7219
+ /** @internal Untracked put body — call {@link put}, not this. */
7220
+ async putInternal(id, record, options) {
7130
7221
  if (!hasWritePermission(this.keyring, this.name)) {
7131
7222
  throw new ReadOnlyError();
7132
7223
  }
@@ -7503,8 +7594,71 @@ var init_collection = __esm({
7503
7594
  }
7504
7595
  }
7505
7596
  }
7506
- /** Delete a record by ID. */
7597
+ /**
7598
+ * Delete a record by ID. Runs inside the hub's write-queue tracker
7599
+ * (#227) so `hub.writeQueue.pending` reflects this write.
7600
+ */
7507
7601
  async delete(id) {
7602
+ await this.schemaUpdateGate?.assertWritable();
7603
+ await this.schemaFence?.assertWritable(this.name);
7604
+ let event;
7605
+ if (this.#hooksActive()) {
7606
+ const prior = await this.#priorForHook(id);
7607
+ event = {
7608
+ op: "delete",
7609
+ vault: this.vault,
7610
+ collection: this.name,
7611
+ docId: id,
7612
+ before: prior.record,
7613
+ after: null,
7614
+ userId: this.keyring.userId,
7615
+ timestamp: Date.now(),
7616
+ txId: this.#txIdForHook(),
7617
+ baseVersion: prior.version,
7618
+ version: prior.version + 1
7619
+ };
7620
+ await this.writeHooks.runBefore(event);
7621
+ }
7622
+ if (this.writeQueue) await this.writeQueue.track(() => this.deleteInternal(id));
7623
+ else await this.deleteInternal(id);
7624
+ if (event) await this.writeHooks.runAfter(event);
7625
+ }
7626
+ /**
7627
+ * @internal #232 — bulk-rewrite every record through a cutover transform.
7628
+ * Raw adapter path (bypasses the write gate + guards — the transform is
7629
+ * trusted and runs only during the `migrating` phase). Bumps each
7630
+ * record's `_v` and appends a ledger `op:'migration'` entry.
7631
+ */
7632
+ async _applyCutoverTransform(transform) {
7633
+ const ids = await this.adapter.list(this.vault, this.name);
7634
+ let count = 0;
7635
+ for (const id of ids) {
7636
+ const env = await this.adapter.get(this.vault, this.name, id);
7637
+ if (!env) continue;
7638
+ const record = await this.decryptRecord(env, { skipValidation: true });
7639
+ const next = transform(record);
7640
+ const nextVersion = (env._v ?? 0) + 1;
7641
+ const newEnv = await this.encryptRecord(next, nextVersion);
7642
+ await this.adapter.put(this.vault, this.name, id, newEnv);
7643
+ await this._invalidateCacheEntry(id);
7644
+ if (this.ledger) {
7645
+ await this.ledger.append({
7646
+ op: "migration",
7647
+ collection: this.name,
7648
+ id,
7649
+ version: nextVersion,
7650
+ actor: this.keyring.userId,
7651
+ payloadHash: "",
7652
+ reason: "schema:coordinated-cutover"
7653
+ }).catch(() => {
7654
+ });
7655
+ }
7656
+ count++;
7657
+ }
7658
+ return count;
7659
+ }
7660
+ /** @internal Untracked delete body — call {@link delete}, not this. */
7661
+ async deleteInternal(id) {
7508
7662
  await this._doDelete(id, false);
7509
7663
  }
7510
7664
  /**
@@ -8327,6 +8481,21 @@ var init_collection = __esm({
8327
8481
  this.cache.set(id, { record, version: envelope._v });
8328
8482
  this.indexes?.upsert(id, record, previous ? previous.record : null);
8329
8483
  }
8484
+ /**
8485
+ * #228b — apply a peer tab's committed write to THIS tab's in-memory view:
8486
+ * re-read the (already-persisted) envelope from the shared store + refresh
8487
+ * cache/indexes, then emit a `change` event so reactive consumers re-render.
8488
+ * Never writes to the store and never fires write hooks, so it cannot loop.
8489
+ */
8490
+ async _applyRemoteChange(id, action) {
8491
+ await this._invalidateCacheEntry(id);
8492
+ this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action });
8493
+ }
8494
+ /** @internal #228c — the current in-memory record without a store read (for conflict capture). */
8495
+ _peekCached(id) {
8496
+ const entry = this.lazy && this.lru ? this.lru.get(id) : this.cache.get(id);
8497
+ return entry ? entry.record : null;
8498
+ }
8330
8499
  async ensureHydrated() {
8331
8500
  if (this.hydrated) return;
8332
8501
  const ids = await this.adapter.list(this.vault, this.name);
@@ -10202,15 +10371,79 @@ var init_derive = __esm({
10202
10371
  }
10203
10372
  });
10204
10373
 
10374
+ // src/schema-update/delta.ts
10375
+ function computeSchemaDelta(stored, fresh, collection) {
10376
+ const a = stored;
10377
+ const b = fresh;
10378
+ const aProps = a.properties ?? {};
10379
+ const bProps = b.properties ?? {};
10380
+ const aReq = new Set(a.required ?? []);
10381
+ const bReq = new Set(b.required ?? []);
10382
+ const aKeys = Object.keys(aProps);
10383
+ const bKeys = Object.keys(bProps);
10384
+ const added = bKeys.filter((k) => !(k in aProps));
10385
+ const removed = aKeys.filter((k) => !(k in bProps));
10386
+ const changed = [];
10387
+ for (const k of bKeys) {
10388
+ if (!(k in aProps)) continue;
10389
+ const shapeChanged = canonicalize(aProps[k]) !== canonicalize(bProps[k]);
10390
+ const requiredChanged = aReq.has(k) !== bReq.has(k);
10391
+ if (shapeChanged || requiredChanged) {
10392
+ changed.push({ field: k, requiredChanged, shapeChanged });
10393
+ }
10394
+ }
10395
+ let kind;
10396
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
10397
+ kind = "none";
10398
+ } else if (removed.length === 0 && changed.length === 0 && added.every((k) => !bReq.has(k))) {
10399
+ kind = "additive";
10400
+ } else {
10401
+ kind = "non-additive";
10402
+ }
10403
+ return { collection, kind, added, removed, changed };
10404
+ }
10405
+ var init_delta = __esm({
10406
+ "src/schema-update/delta.ts"() {
10407
+ "use strict";
10408
+ init_canonicalize();
10409
+ }
10410
+ });
10411
+
10412
+ // src/schema-update/dispatch.ts
10413
+ async function evaluateStrategies(delta, strategies, ctx) {
10414
+ for (const strategy of strategies) {
10415
+ const decision = await strategy.onSchemaDelta(delta, ctx);
10416
+ if (decision.action !== "allow") return decision;
10417
+ }
10418
+ return { action: "allow" };
10419
+ }
10420
+ var init_dispatch = __esm({
10421
+ "src/schema-update/dispatch.ts"() {
10422
+ "use strict";
10423
+ }
10424
+ });
10425
+
10205
10426
  // src/persisted-schemas/register.ts
10206
10427
  async function persistSchemaIfNeeded(opts) {
10207
10428
  const fresh = await derivePersistedSchema(opts.validator);
10208
10429
  const stored = await loadPersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek);
10209
10430
  if (stored && isEquivalent(stored, fresh)) {
10210
- return { written: false, skipped: true, envelope: stored };
10431
+ return { written: false, skipped: true, envelope: stored, decision: { action: "allow" } };
10432
+ }
10433
+ let decision = { action: "allow" };
10434
+ const strategies = opts.strategies ?? [];
10435
+ if (stored && strategies.length > 0 && stored.kind === fresh.kind && isPlainObject2(stored.jsonSchema) && isPlainObject2(fresh.jsonSchema)) {
10436
+ const delta = computeSchemaDelta(stored.jsonSchema, fresh.jsonSchema, opts.collectionName);
10437
+ decision = await evaluateStrategies(delta, strategies, { collection: opts.collectionName });
10438
+ }
10439
+ if (decision.action !== "allow") {
10440
+ return { written: false, skipped: false, envelope: stored ?? fresh, decision };
10211
10441
  }
10212
10442
  await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, fresh);
10213
- return { written: true, skipped: false, envelope: fresh };
10443
+ return { written: true, skipped: false, envelope: fresh, decision };
10444
+ }
10445
+ function isPlainObject2(v) {
10446
+ return typeof v === "object" && v !== null && !Array.isArray(v);
10214
10447
  }
10215
10448
  function isEquivalent(a, b) {
10216
10449
  if (a.kind !== b.kind) return false;
@@ -10223,6 +10456,293 @@ var init_register = __esm({
10223
10456
  "use strict";
10224
10457
  init_derive();
10225
10458
  init_storage2();
10459
+ init_delta();
10460
+ init_dispatch();
10461
+ }
10462
+ });
10463
+
10464
+ // src/schema-update/gate.ts
10465
+ var SchemaUpdateGate;
10466
+ var init_gate = __esm({
10467
+ "src/schema-update/gate.ts"() {
10468
+ "use strict";
10469
+ SchemaUpdateGate = class {
10470
+ #decision;
10471
+ constructor(decision) {
10472
+ this.#decision = decision.catch(() => null);
10473
+ }
10474
+ async assertWritable() {
10475
+ const decision = await this.#decision;
10476
+ if (decision && decision.action === "reject") {
10477
+ throw decision.error;
10478
+ }
10479
+ }
10480
+ };
10481
+ }
10482
+ });
10483
+
10484
+ // src/schema-update/fence.ts
10485
+ async function loadFence(store, vault) {
10486
+ const envelope = await store.get(vault, META_COLLECTION3, FENCE_RECORD_ID);
10487
+ if (!envelope) return DEFAULT_FENCE;
10488
+ try {
10489
+ const parsed = JSON.parse(envelope._data);
10490
+ if (!isFenceDoc(parsed)) return DEFAULT_FENCE;
10491
+ return parsed;
10492
+ } catch {
10493
+ return DEFAULT_FENCE;
10494
+ }
10495
+ }
10496
+ async function saveFence(store, vault, fence) {
10497
+ const envelope = {
10498
+ _noydb: NOYDB_FORMAT_VERSION,
10499
+ _v: 1,
10500
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
10501
+ _iv: "",
10502
+ _data: JSON.stringify(fence)
10503
+ };
10504
+ await store.put(vault, META_COLLECTION3, FENCE_RECORD_ID, envelope);
10505
+ }
10506
+ function isFenceDoc(x) {
10507
+ if (x === null || typeof x !== "object") return false;
10508
+ const o = x;
10509
+ return typeof o["currentSchemaVersion"] === "number" && (o["fenceState"] === "normal" || o["fenceState"] === "draining" || o["fenceState"] === "migrating" || o["fenceState"] === "complete");
10510
+ }
10511
+ var FENCE_RECORD_ID, META_COLLECTION3, DEFAULT_FENCE;
10512
+ var init_fence = __esm({
10513
+ "src/schema-update/fence.ts"() {
10514
+ "use strict";
10515
+ init_types();
10516
+ FENCE_RECORD_ID = "schema-fence";
10517
+ META_COLLECTION3 = "_meta";
10518
+ DEFAULT_FENCE = { currentSchemaVersion: 0, fenceState: "normal" };
10519
+ }
10520
+ });
10521
+
10522
+ // src/schema-update/client-registry.ts
10523
+ async function writeClientDoc(store, vault, clientId, doc) {
10524
+ const envelope = {
10525
+ _noydb: NOYDB_FORMAT_VERSION,
10526
+ _v: 1,
10527
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
10528
+ _iv: "",
10529
+ _data: JSON.stringify({ clientId, ...doc })
10530
+ };
10531
+ await store.put(vault, META_COLLECTION4, `${CLIENT_PREFIX}${clientId}`, envelope);
10532
+ }
10533
+ async function listClientDocs(store, vault) {
10534
+ const ids = await store.list(vault, META_COLLECTION4);
10535
+ const out = [];
10536
+ for (const id of ids) {
10537
+ if (!id.startsWith(CLIENT_PREFIX)) continue;
10538
+ const env = await store.get(vault, META_COLLECTION4, id);
10539
+ if (!env) continue;
10540
+ try {
10541
+ const parsed = JSON.parse(env._data);
10542
+ if (isClientDoc(parsed)) out.push(parsed);
10543
+ } catch {
10544
+ }
10545
+ }
10546
+ return out;
10547
+ }
10548
+ async function activeQuiesced(store, vault, opts) {
10549
+ const docs = await listClientDocs(store, vault);
10550
+ const active = docs.filter(
10551
+ (d) => d.lastSeen >= opts.now - opts.staleMs && d.clientId !== opts.excludeClientId
10552
+ );
10553
+ return active.every((d) => d.quiescedAtVersion === opts.generation);
10554
+ }
10555
+ function isClientDoc(x) {
10556
+ if (x === null || typeof x !== "object") return false;
10557
+ const o = x;
10558
+ return typeof o["clientId"] === "string" && typeof o["lastSeen"] === "number" && (o["quiescedAtVersion"] === null || typeof o["quiescedAtVersion"] === "number");
10559
+ }
10560
+ var META_COLLECTION4, CLIENT_PREFIX;
10561
+ var init_client_registry = __esm({
10562
+ "src/schema-update/client-registry.ts"() {
10563
+ "use strict";
10564
+ init_types();
10565
+ META_COLLECTION4 = "_meta";
10566
+ CLIENT_PREFIX = "schema-fence:client:";
10567
+ }
10568
+ });
10569
+
10570
+ // src/schema-update/fence-controller.ts
10571
+ function delay(ms) {
10572
+ return new Promise((resolve) => setTimeout(resolve, ms));
10573
+ }
10574
+ var SchemaFenceController;
10575
+ var init_fence_controller = __esm({
10576
+ "src/schema-update/fence-controller.ts"() {
10577
+ "use strict";
10578
+ init_fence();
10579
+ init_errors();
10580
+ init_client_registry();
10581
+ SchemaFenceController = class {
10582
+ #store;
10583
+ #vault;
10584
+ #onFlush;
10585
+ #clientId;
10586
+ #now;
10587
+ #staleMs;
10588
+ #quiesceTimeoutMs;
10589
+ #emit;
10590
+ #snapshot = 0;
10591
+ #pending = /* @__PURE__ */ new Map();
10592
+ constructor(opts) {
10593
+ this.#store = opts.store;
10594
+ this.#vault = opts.vault;
10595
+ this.#onFlush = opts.onFlush;
10596
+ this.#clientId = opts.clientId ?? "migrator";
10597
+ this.#now = opts.now ?? (() => Date.now());
10598
+ this.#staleMs = opts.staleMs ?? 3e4;
10599
+ this.#quiesceTimeoutMs = opts.quiesceTimeoutMs ?? 6e4;
10600
+ this.#emit = opts.emit ?? (() => {
10601
+ });
10602
+ }
10603
+ /** Capture the generation snapshot at vault-open. */
10604
+ async init() {
10605
+ this.#snapshot = (await loadFence(this.#store, this.#vault)).currentSchemaVersion;
10606
+ }
10607
+ /** Record a per-collection pending cutover (from a registration `cutover` decision). */
10608
+ registerPendingCutover(collection, transform) {
10609
+ this.#pending.set(collection, transform);
10610
+ }
10611
+ /** Write-path gate. Throws when behind, fenced, or this collection is cutover-pending. */
10612
+ async assertWritable(collection) {
10613
+ const fence = await loadFence(this.#store, this.#vault);
10614
+ if (fence.currentSchemaVersion > this.#snapshot) {
10615
+ throw new MigrationRequiredError(
10616
+ `Vault "${this.#vault}" advanced to schema generation ${fence.currentSchemaVersion} (this client opened at ${this.#snapshot}). Reload to continue.`
10617
+ );
10618
+ }
10619
+ if (fence.fenceState === "draining" || fence.fenceState === "migrating") {
10620
+ throw new SchemaFenceError(`Vault "${this.#vault}" is mid-cutover (${fence.fenceState}); writes are paused.`);
10621
+ }
10622
+ if (this.#pending.has(collection)) {
10623
+ throw new SchemaFenceError(
10624
+ `Collection "${collection}" has a pending schema cutover; run vault.runSchemaCutover() before writing.`
10625
+ );
10626
+ }
10627
+ }
10628
+ /**
10629
+ * Admin trigger. Drain → wait for the active set to quiesce (or time out)
10630
+ * → migrate each pending transform → bump → complete → normal. The
10631
+ * migrator excludes itself from the barrier (it drained synchronously
10632
+ * here). `onPoll` (tests) advances other clients between barrier checks;
10633
+ * production falls back to a short real delay.
10634
+ */
10635
+ async runCutover(run, opts) {
10636
+ if (this.#pending.size === 0) return { migrated: 0 };
10637
+ const base = await loadFence(this.#store, this.#vault);
10638
+ const generation = base.currentSchemaVersion;
10639
+ await this.#setState(generation, "draining");
10640
+ await this.#onFlush();
10641
+ const deadline = this.#now() + this.#quiesceTimeoutMs;
10642
+ while (!await activeQuiesced(this.#store, this.#vault, {
10643
+ generation,
10644
+ now: this.#now(),
10645
+ staleMs: this.#staleMs,
10646
+ excludeClientId: this.#clientId
10647
+ })) {
10648
+ if (this.#now() >= deadline) {
10649
+ throw new QuiesceTimeoutError(
10650
+ `Cutover on "${this.#vault}" timed out waiting for clients to quiesce at generation ${generation}.`
10651
+ );
10652
+ }
10653
+ await (opts?.onPoll ? opts.onPoll() : delay(50));
10654
+ }
10655
+ await this.#setState(generation, "migrating");
10656
+ let migrated = 0;
10657
+ for (const [collection, transform] of this.#pending) {
10658
+ await run(collection, transform);
10659
+ migrated++;
10660
+ }
10661
+ const nextVersion = generation + 1;
10662
+ await this.#setState(nextVersion, "complete");
10663
+ this.#pending.clear();
10664
+ await this.#setState(nextVersion, "normal");
10665
+ this.#snapshot = nextVersion;
10666
+ return { migrated };
10667
+ }
10668
+ /** Recover a stuck drain: reset fenceState to normal at the current version (no bump). */
10669
+ async abort() {
10670
+ const fence = await loadFence(this.#store, this.#vault);
10671
+ await this.#setState(fence.currentSchemaVersion, "normal");
10672
+ }
10673
+ async #setState(currentSchemaVersion, fenceState) {
10674
+ await saveFence(this.#store, this.#vault, { currentSchemaVersion, fenceState });
10675
+ this.#emit({ currentSchemaVersion, fenceState });
10676
+ }
10677
+ };
10678
+ }
10679
+ });
10680
+
10681
+ // src/schema-update/fence-watcher.ts
10682
+ var FenceWatcher;
10683
+ var init_fence_watcher = __esm({
10684
+ "src/schema-update/fence-watcher.ts"() {
10685
+ "use strict";
10686
+ init_fence();
10687
+ init_client_registry();
10688
+ FenceWatcher = class {
10689
+ #store;
10690
+ #vault;
10691
+ #clientId;
10692
+ #onFlush;
10693
+ #now;
10694
+ #emit;
10695
+ #lastState = null;
10696
+ #quiescedAtVersion = null;
10697
+ #timer;
10698
+ constructor(opts) {
10699
+ this.#store = opts.store;
10700
+ this.#vault = opts.vault;
10701
+ this.#clientId = opts.clientId;
10702
+ this.#onFlush = opts.onFlush;
10703
+ this.#now = opts.now ?? (() => Date.now());
10704
+ this.#emit = opts.emit ?? (() => {
10705
+ });
10706
+ }
10707
+ /** Publish liveness (and the current ack) without changing quiesce state. */
10708
+ async beat() {
10709
+ await writeClientDoc(this.#store, this.#vault, this.#clientId, {
10710
+ lastSeen: this.#now(),
10711
+ quiescedAtVersion: this.#quiescedAtVersion
10712
+ });
10713
+ }
10714
+ /** Poll the fence; quiesce on draining; emit on transitions. */
10715
+ async check() {
10716
+ const fence = await loadFence(this.#store, this.#vault);
10717
+ if (fence.fenceState !== this.#lastState) {
10718
+ this.#lastState = fence.fenceState;
10719
+ this.#emit({ currentSchemaVersion: fence.currentSchemaVersion, fenceState: fence.fenceState });
10720
+ }
10721
+ if (fence.fenceState === "draining" && this.#quiescedAtVersion !== fence.currentSchemaVersion) {
10722
+ await this.#onFlush();
10723
+ this.#quiescedAtVersion = fence.currentSchemaVersion;
10724
+ await this.beat();
10725
+ }
10726
+ if (fence.fenceState === "normal") {
10727
+ this.#quiescedAtVersion = null;
10728
+ }
10729
+ }
10730
+ start(intervalMs) {
10731
+ if (this.#timer) return;
10732
+ this.#timer = setInterval(() => {
10733
+ void this.beat();
10734
+ void this.check();
10735
+ }, intervalMs);
10736
+ const timer = this.#timer;
10737
+ if (typeof timer.unref === "function") timer.unref();
10738
+ }
10739
+ stop() {
10740
+ if (this.#timer) {
10741
+ clearInterval(this.#timer);
10742
+ this.#timer = void 0;
10743
+ }
10744
+ }
10745
+ };
10226
10746
  }
10227
10747
  });
10228
10748
 
@@ -10684,6 +11204,13 @@ var init_registry2 = __esm({
10684
11204
  guardsFor(collection) {
10685
11205
  return this._byCollection.get(collection) ?? [];
10686
11206
  }
11207
+ /** Per-collection guard counts, for introspection (#229). */
11208
+ summary() {
11209
+ return [...this._byCollection.entries()].map(([collection, guards]) => ({
11210
+ collection,
11211
+ count: guards.length
11212
+ }));
11213
+ }
10687
11214
  /**
10688
11215
  * Run every guard's `check` for this collection. First throw wins —
10689
11216
  * remaining guards are not invoked. Guards without a `check` skip.
@@ -11082,6 +11609,10 @@ var init_vault = __esm({
11082
11609
  init_magic_link_grant();
11083
11610
  init_api();
11084
11611
  init_register();
11612
+ init_gate();
11613
+ init_fence_controller();
11614
+ init_fence_watcher();
11615
+ init_fence();
11085
11616
  init_storage2();
11086
11617
  init_walk();
11087
11618
  init_types2();
@@ -11188,6 +11719,13 @@ var init_vault = __esm({
11188
11719
  */
11189
11720
  reloadKeyring;
11190
11721
  collectionCache = /* @__PURE__ */ new Map();
11722
+ /** #232 — vault-level schema cutover fence/controller. */
11723
+ schemaFence;
11724
+ /** #232 — per-client heartbeat/watcher; started lazily on cutover registration. */
11725
+ #fenceWatcher;
11726
+ #fenceCoordinationStarted = false;
11727
+ /** #229 — per-collection registered schema-update strategy names. */
11728
+ #schemaUpdateNames = /* @__PURE__ */ new Map();
11191
11729
  /**
11192
11730
  * per-collection `blobFields` retention/TTL config.
11193
11731
  * Populated on `collection({ blobFields })` and read by
@@ -11303,6 +11841,13 @@ var init_vault = __esm({
11303
11841
  this.noydb = opts.noydb;
11304
11842
  this.keyring = opts.keyring;
11305
11843
  this.encrypted = opts.encrypted;
11844
+ this.schemaFence = new SchemaFenceController({
11845
+ store: this.adapter,
11846
+ vault: this.name,
11847
+ onFlush: () => this.noydb._writeQueueTracker.onFlush(),
11848
+ clientId: this.noydb._clientId,
11849
+ emit: (e) => this.emitter.emit("schema:fence-changed", { vault: this.name, ...e })
11850
+ });
11306
11851
  this.emitter = opts.emitter;
11307
11852
  this.onDirty = opts.onDirty;
11308
11853
  this.onRegisterConflictResolver = opts.onRegisterConflictResolver;
@@ -11411,6 +11956,35 @@ var init_vault = __esm({
11411
11956
  }
11412
11957
  this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
11413
11958
  }
11959
+ if ((options?.schemaUpdate?.length ?? 0) > 0) {
11960
+ this.#schemaUpdateNames.set(collectionName, (options.schemaUpdate ?? []).map((s) => s.name));
11961
+ }
11962
+ let schemaUpdateGate;
11963
+ if (options?.persistJsonSchema === true && options.schema !== void 0 && (options.schemaUpdate?.length ?? 0) > 0) {
11964
+ const validator = options.schema;
11965
+ const strategies = options.schemaUpdate ?? [];
11966
+ const work = (async () => {
11967
+ const dek = await this.getDEK(collectionName);
11968
+ const result = await persistSchemaIfNeeded({
11969
+ store: this.adapter,
11970
+ vault: this.name,
11971
+ collectionName,
11972
+ validator,
11973
+ dek,
11974
+ strategies
11975
+ });
11976
+ const decision = result.decision ?? { action: "allow" };
11977
+ if (decision.action === "cutover") {
11978
+ this.schemaFence.registerPendingCutover(collectionName, decision.transform);
11979
+ this._ensureFenceCoordination();
11980
+ }
11981
+ return decision;
11982
+ })();
11983
+ this._pendingSchemaWrites.push(work.then(() => {
11984
+ }, () => {
11985
+ }));
11986
+ schemaUpdateGate = new SchemaUpdateGate(work);
11987
+ }
11414
11988
  const collOpts = {
11415
11989
  adapter: this.adapter,
11416
11990
  vault: this.name,
@@ -11418,6 +11992,11 @@ var init_vault = __esm({
11418
11992
  keyring: this.keyring,
11419
11993
  encrypted: this.encrypted,
11420
11994
  emitter: this.emitter,
11995
+ writeQueue: this.noydb._writeQueueTracker,
11996
+ writeHooks: this.noydb._writeHooks,
11997
+ activeTxId: () => this.noydb._activeTxContextOrNull?.txId ?? null,
11998
+ schemaUpdateGate,
11999
+ schemaFence: this.schemaFence,
11421
12000
  getDEK: this.getDEK,
11422
12001
  onDirty: this.onDirty,
11423
12002
  historyConfig: this.historyConfig,
@@ -11504,7 +12083,7 @@ var init_vault = __esm({
11504
12083
  }
11505
12084
  coll = new Collection(collOpts);
11506
12085
  this.collectionCache.set(collectionName, coll);
11507
- if (options?.persistJsonSchema === true && options.schema !== void 0) {
12086
+ if (options?.persistJsonSchema === true && options.schema !== void 0 && (options.schemaUpdate?.length ?? 0) === 0) {
11508
12087
  const validator = options.schema;
11509
12088
  const work = (async () => {
11510
12089
  try {
@@ -11537,6 +12116,87 @@ var init_vault = __esm({
11537
12116
  this._pendingSchemaWrites = [];
11538
12117
  await Promise.allSettled(pending);
11539
12118
  }
12119
+ /**
12120
+ * Run a coordinated schema cutover (#232). Drains pending writes, waits
12121
+ * for the active client set to quiesce (the ack-barrier), applies every
12122
+ * pending collection transform in bulk, bumps the vault schema generation,
12123
+ * and clears the fence. Returns the count of collections migrated.
12124
+ * `opts.onPoll` (tests) advances other clients between barrier checks.
12125
+ */
12126
+ async runSchemaCutover(opts) {
12127
+ return this.schemaFence.runCutover(
12128
+ (collectionName, transform) => this.#runCutoverTransform(collectionName, transform),
12129
+ opts
12130
+ );
12131
+ }
12132
+ async #runCutoverTransform(collectionName, transform) {
12133
+ const coll = this.collectionCache.get(collectionName);
12134
+ if (!coll) return;
12135
+ await coll._applyCutoverTransform(transform);
12136
+ }
12137
+ /**
12138
+ * #228b — refresh a loaded collection's view of one document from a peer
12139
+ * tab's broadcast. No-op when the collection isn't loaded in this tab
12140
+ * (it will read fresh on next open). Mirrors #runCutoverTransform's guard.
12141
+ */
12142
+ async _applyRemoteWrite(collectionName, docId, action) {
12143
+ const coll = this.collectionCache.get(collectionName);
12144
+ if (!coll) return;
12145
+ await coll._applyRemoteChange(docId, action);
12146
+ }
12147
+ /**
12148
+ * #228c — for a detected conflict: capture this tab's clobbered record,
12149
+ * read the common ancestor from history, converge the cache to the store's
12150
+ * authoritative value (the (b) re-read), and return all three for the
12151
+ * WriteConflict payload. Returns null when the collection isn't loaded.
12152
+ */
12153
+ async _captureAndConverge(collectionName, docId, action, baseV) {
12154
+ const coll = this.collectionCache.get(collectionName);
12155
+ if (!coll) return null;
12156
+ const local = coll._peekCached(docId);
12157
+ let base = null;
12158
+ try {
12159
+ base = await coll.getVersion(docId, baseV);
12160
+ } catch {
12161
+ base = null;
12162
+ }
12163
+ await coll._applyRemoteChange(docId, action);
12164
+ const remote = await coll.get(docId);
12165
+ return { local, remote, base };
12166
+ }
12167
+ /** Recover a stuck cutover fence (#232) — reset to normal without bumping. */
12168
+ async abortSchemaCutover() {
12169
+ await this.schemaFence.abort();
12170
+ }
12171
+ /** Current schema-cutover fence state for this vault (#232/#233). Thin live read. */
12172
+ async schemaFenceState() {
12173
+ return loadFence(this.adapter, this.name);
12174
+ }
12175
+ /** @internal Start the per-client heartbeat + fence watcher once a cutover is registered (#232). */
12176
+ _ensureFenceCoordination() {
12177
+ if (this.#fenceCoordinationStarted) return;
12178
+ this.#fenceCoordinationStarted = true;
12179
+ this.#fenceWatcher = new FenceWatcher({
12180
+ store: this.adapter,
12181
+ vault: this.name,
12182
+ clientId: this.noydb._clientId,
12183
+ onFlush: () => this.noydb._writeQueueTracker.onFlush(),
12184
+ emit: (e) => this.emitter.emit("schema:fence-changed", { vault: this.name, ...e })
12185
+ });
12186
+ this.#fenceWatcher.start(2e3);
12187
+ }
12188
+ /** @internal Stop the heartbeat/watcher (vault lock/close). */
12189
+ _stopFenceCoordination() {
12190
+ this.#fenceWatcher?.stop();
12191
+ this.#fenceWatcher = void 0;
12192
+ this.#fenceCoordinationStarted = false;
12193
+ }
12194
+ /** @internal Drive one heartbeat + watch cycle deterministically (tests). */
12195
+ async _fenceTick() {
12196
+ this._ensureFenceCoordination();
12197
+ await this.#fenceWatcher.beat();
12198
+ await this.#fenceWatcher.check();
12199
+ }
11540
12200
  /**
11541
12201
  * Validate i18nText fields on a `put()`. Called by Collection just
11542
12202
  * before the adapter write, after schema validation. Throws
@@ -12965,6 +13625,27 @@ var init_vault = __esm({
12965
13625
  async dumpSchema(opts = {}) {
12966
13626
  return dumpVaultSchema(this, opts);
12967
13627
  }
13628
+ /**
13629
+ * Lightweight read of the vault's registered schema (#229): collections
13630
+ * (+ doc counts), guards, materialized views, schema-update strategies,
13631
+ * and the unlocked user's grants. Cheap — one `adapter.list` per
13632
+ * collection, no decryption. For a full snapshot + stats use dumpSchema().
13633
+ * Post-unlock by construction (a Vault only exists with an unlocked keyring).
13634
+ */
13635
+ async introspect() {
13636
+ const byCol = (a, b) => a.collection.localeCompare(b.collection);
13637
+ const names = [.../* @__PURE__ */ new Set([...this.collectionCache.keys(), ...await this.collections()])].filter((n) => !n.startsWith("_")).sort((a, b) => a.localeCompare(b));
13638
+ const collections = [];
13639
+ for (const name of names) {
13640
+ const ids = await this.adapter.list(this.name, name);
13641
+ collections.push({ name, docCount: ids.length });
13642
+ }
13643
+ const guards = (this._getGuardRegistry()?.summary() ?? []).slice().sort(byCol);
13644
+ const materializedViews = (this._getMaterializedViewRegistry()?.all() ?? []).map((mv) => ({ name: mv.spec.name, sourceCollections: [...mv.dependencies].sort() })).sort((a, b) => a.name.localeCompare(b.name));
13645
+ const schemaUpdate = [...this.#schemaUpdateNames.entries()].map(([collection, strategies]) => ({ collection, strategies })).sort(byCol);
13646
+ const grants = [...this.keyring.deks.keys()].filter((collection) => !collection.startsWith("_")).map((collection) => ({ collection, permission: this.keyring.permissions[collection] ?? "rw" })).sort(byCol);
13647
+ return { collections, guards, materializedViews, schemaUpdate, grants };
13648
+ }
12968
13649
  /**
12969
13650
  * Internal accessor for {@link dumpVaultSchema}. Exposes the structural
12970
13651
  * state the walker needs (collection cache, registries, ref registry,
@@ -13612,6 +14293,411 @@ var init_events = __esm({
13612
14293
  }
13613
14294
  });
13614
14295
 
14296
+ // src/write-queue.ts
14297
+ var WriteQueueTracker;
14298
+ var init_write_queue = __esm({
14299
+ "src/write-queue.ts"() {
14300
+ "use strict";
14301
+ WriteQueueTracker = class {
14302
+ #depth = 0;
14303
+ #error = null;
14304
+ #changeHandlers = /* @__PURE__ */ new Set();
14305
+ #flushWaiters = [];
14306
+ get pending() {
14307
+ return this.#depth > 0;
14308
+ }
14309
+ get depth() {
14310
+ return this.#depth;
14311
+ }
14312
+ /** Mark one write as started. */
14313
+ begin() {
14314
+ this.#depth++;
14315
+ this.#emitChange();
14316
+ }
14317
+ /** Mark one write as finished. Pass the error if it failed. */
14318
+ settle(error) {
14319
+ this.#depth = Math.max(0, this.#depth - 1);
14320
+ if (error) this.#error = error;
14321
+ this.#emitChange();
14322
+ if (this.#depth === 0) this.#drainFlush();
14323
+ }
14324
+ onChange(handler) {
14325
+ this.#changeHandlers.add(handler);
14326
+ return () => {
14327
+ this.#changeHandlers.delete(handler);
14328
+ };
14329
+ }
14330
+ onFlush() {
14331
+ if (this.#depth === 0) {
14332
+ const error = this.#error;
14333
+ this.#error = null;
14334
+ return error ? Promise.reject(error) : Promise.resolve();
14335
+ }
14336
+ return new Promise((resolve, reject) => {
14337
+ this.#flushWaiters.push({ resolve, reject });
14338
+ });
14339
+ }
14340
+ /**
14341
+ * Run `fn` as a tracked write: depth++ on entry, depth-- on settle
14342
+ * (success or failure). The fn's resolved value is returned; a thrown
14343
+ * error is re-thrown after the queue is decremented.
14344
+ */
14345
+ async track(fn) {
14346
+ this.begin();
14347
+ try {
14348
+ const value = await fn();
14349
+ this.settle();
14350
+ return value;
14351
+ } catch (error) {
14352
+ this.settle(error);
14353
+ throw error;
14354
+ }
14355
+ }
14356
+ #emitChange() {
14357
+ for (const handler of this.#changeHandlers) handler();
14358
+ }
14359
+ #drainFlush() {
14360
+ const waiters = this.#flushWaiters;
14361
+ this.#flushWaiters = [];
14362
+ const error = this.#error;
14363
+ this.#error = null;
14364
+ for (const waiter of waiters) {
14365
+ if (error) waiter.reject(error);
14366
+ else waiter.resolve();
14367
+ }
14368
+ }
14369
+ };
14370
+ }
14371
+ });
14372
+
14373
+ // src/write-hooks.ts
14374
+ var WriteHookRegistry;
14375
+ var init_write_hooks = __esm({
14376
+ "src/write-hooks.ts"() {
14377
+ "use strict";
14378
+ WriteHookRegistry = class {
14379
+ #before = [];
14380
+ #after = [];
14381
+ #suppressed = false;
14382
+ /** True while handlers are running — used by the write path to skip nested firing. */
14383
+ get suppressed() {
14384
+ return this.#suppressed;
14385
+ }
14386
+ /** True when any hook is registered (cheap gate for the write path). */
14387
+ get hasHandlers() {
14388
+ return this.#before.length > 0 || this.#after.length > 0;
14389
+ }
14390
+ onBeforeWrite(handler) {
14391
+ this.#before.push(handler);
14392
+ return () => {
14393
+ const i = this.#before.indexOf(handler);
14394
+ if (i >= 0) this.#before.splice(i, 1);
14395
+ };
14396
+ }
14397
+ onAfterWrite(handler) {
14398
+ this.#after.push(handler);
14399
+ return () => {
14400
+ const i = this.#after.indexOf(handler);
14401
+ if (i >= 0) this.#after.splice(i, 1);
14402
+ };
14403
+ }
14404
+ /** Run before-hooks (awaited, in order). A throw propagates and aborts the write. */
14405
+ async runBefore(event) {
14406
+ if (this.#before.length === 0) return;
14407
+ this.#suppressed = true;
14408
+ try {
14409
+ for (const h of this.#before.slice()) await h(event);
14410
+ } finally {
14411
+ this.#suppressed = false;
14412
+ }
14413
+ }
14414
+ /** Run after-hooks (awaited, in order). Per-handler errors are warned, not thrown. */
14415
+ async runAfter(event) {
14416
+ if (this.#after.length === 0) return;
14417
+ this.#suppressed = true;
14418
+ try {
14419
+ for (const h of this.#after.slice()) {
14420
+ try {
14421
+ await h(event);
14422
+ } catch (err) {
14423
+ console.warn(
14424
+ `[noy-db] onAfterWrite handler failed for ${event.collection}/${event.docId}: ` + (err instanceof Error ? err.message : String(err))
14425
+ );
14426
+ }
14427
+ }
14428
+ } finally {
14429
+ this.#suppressed = false;
14430
+ }
14431
+ }
14432
+ };
14433
+ }
14434
+ });
14435
+
14436
+ // src/tab-coordination.ts
14437
+ function isPresenceMsg(x) {
14438
+ if (x === null || typeof x !== "object") return false;
14439
+ const o = x;
14440
+ return o["kind"] === "tab-presence" && typeof o["tabId"] === "string" && typeof o["lastSeen"] === "number" && (o["role"] === "primary" || o["role"] === "secondary" || o["role"] === "unknown");
14441
+ }
14442
+ function cheapRand() {
14443
+ const g = globalThis;
14444
+ return g.crypto?.randomUUID ? g.crypto.randomUUID().slice(0, 8) : "anon";
14445
+ }
14446
+ function defaultLockManager() {
14447
+ const nav = globalThis.navigator;
14448
+ return nav?.locks;
14449
+ }
14450
+ function defaultChannel(name = "noydb:tabs") {
14451
+ if (typeof globalThis.window === "undefined") return void 0;
14452
+ const Bc = globalThis.BroadcastChannel;
14453
+ if (!Bc) return void 0;
14454
+ const bc = new Bc(name);
14455
+ const msgListeners = /* @__PURE__ */ new Set();
14456
+ bc.onmessage = (e) => {
14457
+ for (const l of msgListeners) l(String(e.data));
14458
+ };
14459
+ return {
14460
+ isOpen: true,
14461
+ send(payload) {
14462
+ bc.postMessage(payload);
14463
+ },
14464
+ on(event, listener) {
14465
+ if (event === "message") {
14466
+ const l = listener;
14467
+ msgListeners.add(l);
14468
+ return () => msgListeners.delete(l);
14469
+ }
14470
+ return () => {
14471
+ };
14472
+ },
14473
+ close() {
14474
+ msgListeners.clear();
14475
+ bc.close();
14476
+ }
14477
+ };
14478
+ }
14479
+ var TabCoordinator;
14480
+ var init_tab_coordination = __esm({
14481
+ "src/tab-coordination.ts"() {
14482
+ "use strict";
14483
+ TabCoordinator = class {
14484
+ tabId;
14485
+ role = "unknown";
14486
+ #lockManager;
14487
+ #channel;
14488
+ #lockName;
14489
+ #heartbeatMs;
14490
+ #staleMs;
14491
+ #now;
14492
+ #peers = /* @__PURE__ */ new Map();
14493
+ #roleHandlers = /* @__PURE__ */ new Set();
14494
+ #tabsHandlers = /* @__PURE__ */ new Set();
14495
+ #ac;
14496
+ #releaseLock;
14497
+ #unsub;
14498
+ #closeUnsub;
14499
+ #timer;
14500
+ #ownsChannel;
14501
+ #started = false;
14502
+ #disposed = false;
14503
+ #lastTabsSig = "";
14504
+ constructor(opts = {}) {
14505
+ this.tabId = opts.tabId ?? `tab-${Math.trunc((opts.now ?? (() => 0))())}-${cheapRand()}`;
14506
+ this.#lockManager = opts.lockManager;
14507
+ this.#channel = opts.channel;
14508
+ this.#lockName = opts.lockName ?? "noydb:tab-primary";
14509
+ this.#heartbeatMs = opts.heartbeatMs ?? 2e3;
14510
+ this.#staleMs = opts.staleMs ?? 6e3;
14511
+ this.#now = opts.now ?? (() => Date.now());
14512
+ this.#ownsChannel = opts.closeChannelOnDispose ?? false;
14513
+ }
14514
+ start() {
14515
+ if (this.#disposed || this.#started) return;
14516
+ this.#started = true;
14517
+ if (this.#channel) {
14518
+ this.#unsub = this.#channel.on("message", (p) => this.#onMessage(p));
14519
+ this.#closeUnsub = this.#channel.on("close", () => this.#onChannelClose());
14520
+ this.#beat();
14521
+ this.#timer = setInterval(() => this.#tick(), this.#heartbeatMs);
14522
+ const t = this.#timer;
14523
+ if (typeof t.unref === "function") t.unref();
14524
+ }
14525
+ if (this.#lockManager) {
14526
+ this.#ac = new AbortController();
14527
+ this.#setRole("secondary");
14528
+ void this.#lockManager.request(this.#lockName, { mode: "exclusive", signal: this.#ac.signal }, () => {
14529
+ this.#setRole("primary");
14530
+ return new Promise((resolve) => {
14531
+ this.#releaseLock = resolve;
14532
+ });
14533
+ }).catch(() => {
14534
+ });
14535
+ }
14536
+ }
14537
+ activeTabs() {
14538
+ if (!this.#channel) return [];
14539
+ const cutoff = this.#now() - this.#staleMs;
14540
+ const self = { tabId: this.tabId, lastSeen: this.#now(), role: this.role };
14541
+ const out = [self, ...[...this.#peers.values()].filter((p) => p.lastSeen >= cutoff)];
14542
+ return out.sort((a, b) => a.tabId.localeCompare(b.tabId));
14543
+ }
14544
+ onTabRoleChange(fn) {
14545
+ this.#roleHandlers.add(fn);
14546
+ return () => this.#roleHandlers.delete(fn);
14547
+ }
14548
+ onActiveTabsChange(fn) {
14549
+ this.#tabsHandlers.add(fn);
14550
+ return () => this.#tabsHandlers.delete(fn);
14551
+ }
14552
+ dispose() {
14553
+ if (this.#disposed) return;
14554
+ this.#disposed = true;
14555
+ this.#releaseLock?.();
14556
+ this.#ac?.abort();
14557
+ if (this.#timer) {
14558
+ clearInterval(this.#timer);
14559
+ this.#timer = void 0;
14560
+ }
14561
+ this.#unsub?.();
14562
+ this.#closeUnsub?.();
14563
+ if (this.#ownsChannel) this.#channel?.close();
14564
+ this.#setRole("unknown");
14565
+ }
14566
+ /** @internal test seam — broadcast one heartbeat now. */
14567
+ _beat() {
14568
+ this.#beat();
14569
+ }
14570
+ #tick() {
14571
+ this.#prune();
14572
+ this.#emitTabs();
14573
+ this.#beat();
14574
+ }
14575
+ #beat() {
14576
+ if (this.#disposed) return;
14577
+ if (!this.#channel || !this.#channel.isOpen) return;
14578
+ const msg = { kind: "tab-presence", tabId: this.tabId, lastSeen: this.#now(), role: this.role };
14579
+ this.#channel.send(JSON.stringify(msg));
14580
+ }
14581
+ #onChannelClose() {
14582
+ if (this.#timer) {
14583
+ clearInterval(this.#timer);
14584
+ this.#timer = void 0;
14585
+ }
14586
+ this.#setRole("unknown");
14587
+ }
14588
+ #onMessage(payload) {
14589
+ let msg;
14590
+ try {
14591
+ msg = JSON.parse(payload);
14592
+ } catch {
14593
+ return;
14594
+ }
14595
+ if (!isPresenceMsg(msg) || msg.tabId === this.tabId) return;
14596
+ this.#peers.set(msg.tabId, { tabId: msg.tabId, lastSeen: msg.lastSeen, role: msg.role });
14597
+ this.#prune();
14598
+ this.#emitTabs();
14599
+ }
14600
+ #prune() {
14601
+ const cutoff = this.#now() - this.#staleMs;
14602
+ for (const [id, p] of this.#peers) if (p.lastSeen < cutoff) this.#peers.delete(id);
14603
+ }
14604
+ #setRole(role) {
14605
+ if (this.role === role) return;
14606
+ this.role = role;
14607
+ for (const h of this.#roleHandlers) h(role);
14608
+ this.#beat();
14609
+ this.#emitTabs();
14610
+ }
14611
+ #emitTabs() {
14612
+ const tabs = this.activeTabs();
14613
+ const sig = tabs.map((t) => `${t.tabId}:${t.role}`).join("|");
14614
+ if (sig === this.#lastTabsSig) return;
14615
+ this.#lastTabsSig = sig;
14616
+ for (const h of this.#tabsHandlers) h(tabs);
14617
+ }
14618
+ };
14619
+ }
14620
+ });
14621
+
14622
+ // src/tab-write-relay.ts
14623
+ function ledgerKey(vault, collection, docId) {
14624
+ return `${vault}\0${collection}\0${docId}`;
14625
+ }
14626
+ function isTabWriteMsg(x) {
14627
+ if (x === null || typeof x !== "object") return false;
14628
+ const o = x;
14629
+ return o["kind"] === "tab-write" && typeof o["writerId"] === "string" && typeof o["vault"] === "string" && typeof o["collection"] === "string" && typeof o["docId"] === "string" && (o["action"] === "put" || o["action"] === "delete") && typeof o["baseV"] === "number" && typeof o["v"] === "number";
14630
+ }
14631
+ var CrossTabWriteRelay;
14632
+ var init_tab_write_relay = __esm({
14633
+ "src/tab-write-relay.ts"() {
14634
+ "use strict";
14635
+ CrossTabWriteRelay = class {
14636
+ #channel;
14637
+ #writerId;
14638
+ #subscribeAfterWrite;
14639
+ #applyRemoteWrite;
14640
+ #reportConflict;
14641
+ #ledger = /* @__PURE__ */ new Map();
14642
+ #ownsChannel;
14643
+ #unsubMsg;
14644
+ #unsubWrite;
14645
+ #started = false;
14646
+ #disposed = false;
14647
+ constructor(opts) {
14648
+ this.#channel = opts.channel;
14649
+ this.#writerId = opts.writerId;
14650
+ this.#subscribeAfterWrite = opts.subscribeAfterWrite;
14651
+ this.#applyRemoteWrite = opts.applyRemoteWrite;
14652
+ this.#reportConflict = opts.reportConflict;
14653
+ this.#ownsChannel = opts.closeChannelOnDispose ?? false;
14654
+ }
14655
+ start() {
14656
+ if (this.#started || this.#disposed) return;
14657
+ this.#started = true;
14658
+ this.#unsubMsg = this.#channel.on("message", (p) => this.#onMessage(p));
14659
+ this.#unsubWrite = this.#subscribeAfterWrite((e) => this.#onLocalWrite(e));
14660
+ }
14661
+ dispose() {
14662
+ if (this.#disposed) return;
14663
+ this.#disposed = true;
14664
+ this.#unsubWrite?.();
14665
+ this.#unsubMsg?.();
14666
+ if (this.#ownsChannel) this.#channel.close();
14667
+ }
14668
+ #onLocalWrite(e) {
14669
+ if (this.#disposed || !this.#channel.isOpen) return;
14670
+ this.#ledger.set(ledgerKey(e.vault, e.collection, e.docId), e.version);
14671
+ const action = e.op === "delete" ? "delete" : "put";
14672
+ const msg = { kind: "tab-write", writerId: this.#writerId, vault: e.vault, collection: e.collection, docId: e.docId, action, baseV: e.baseVersion, v: e.version };
14673
+ this.#channel.send(JSON.stringify(msg));
14674
+ }
14675
+ #onMessage(payload) {
14676
+ if (this.#disposed) return;
14677
+ let msg;
14678
+ try {
14679
+ msg = JSON.parse(payload);
14680
+ } catch {
14681
+ return;
14682
+ }
14683
+ if (!isTabWriteMsg(msg) || msg.writerId === this.#writerId) return;
14684
+ const key = ledgerKey(msg.vault, msg.collection, msg.docId);
14685
+ const ownV = this.#ledger.get(key);
14686
+ if (ownV !== void 0 && msg.baseV < ownV && this.#reportConflict) {
14687
+ void Promise.resolve(this.#reportConflict(msg.vault, msg.collection, msg.docId, msg.action, msg.baseV, msg.v, ownV)).catch((err) => {
14688
+ console.warn(`[noy-db] cross-tab conflict report failed for ${msg.collection}/${msg.docId}: ` + (err instanceof Error ? err.message : String(err)));
14689
+ });
14690
+ return;
14691
+ }
14692
+ if (ownV !== void 0 && msg.baseV >= ownV) this.#ledger.set(key, msg.v);
14693
+ void Promise.resolve(this.#applyRemoteWrite(msg.vault, msg.collection, msg.docId, msg.action)).catch((err) => {
14694
+ console.warn(`[noy-db] cross-tab apply failed for ${msg.collection}/${msg.docId}: ` + (err instanceof Error ? err.message : String(err)));
14695
+ });
14696
+ }
14697
+ };
14698
+ }
14699
+ });
14700
+
13615
14701
  // src/team/authenticators.ts
13616
14702
  async function enrollAuthenticator(store, vault, keyring, options) {
13617
14703
  const existing = keyring.authenticators.find((a) => a.id === options.id);
@@ -13759,6 +14845,9 @@ var init_strategy10 = __esm({
13759
14845
  NO_TX = {
13760
14846
  async runTransaction() {
13761
14847
  throw NOT_ENABLED5;
14848
+ },
14849
+ async runDryRun() {
14850
+ throw NOT_ENABLED5;
13762
14851
  }
13763
14852
  };
13764
14853
  }
@@ -14135,6 +15224,10 @@ var init_noydb = __esm({
14135
15224
  init_public_envelope();
14136
15225
  init_vault();
14137
15226
  init_events();
15227
+ init_write_queue();
15228
+ init_write_hooks();
15229
+ init_tab_coordination();
15230
+ init_tab_write_relay();
14138
15231
  init_keyring();
14139
15232
  init_authenticators();
14140
15233
  init_unlock_state();
@@ -14154,6 +15247,9 @@ var init_noydb = __esm({
14154
15247
  Noydb = class {
14155
15248
  options;
14156
15249
  emitter = new NoydbEventEmitter();
15250
+ writeQueueTracker = new WriteQueueTracker();
15251
+ writeHooks = new WriteHookRegistry();
15252
+ clientId = generateULID();
14157
15253
  vaultCache = /* @__PURE__ */ new Map();
14158
15254
  keyringCache = /* @__PURE__ */ new Map();
14159
15255
  syncEngines = /* @__PURE__ */ new Map();
@@ -14186,6 +15282,10 @@ var init_noydb = __esm({
14186
15282
  publicEnvelopeSchema;
14187
15283
  closed = false;
14188
15284
  sessionTimer = null;
15285
+ /** Same-device multi-tab coordinator (#228); created on `enableTabCoordination()`. */
15286
+ tabCoordinator;
15287
+ /** Cross-tab write relay (#228b); created on `enableTabCoordination()`. */
15288
+ writeRelay;
14189
15289
  /** Per-vault policy enforcers. */
14190
15290
  policyEnforcers = /* @__PURE__ */ new Map();
14191
15291
  txStrategy;
@@ -14378,6 +15478,7 @@ var init_noydb = __esm({
14378
15478
  await comp._initDerivations(this.options.derivationStrategies ?? []);
14379
15479
  await comp._initMaterializedViews(this.options.materializedViewStrategies ?? []);
14380
15480
  await comp._initOverlayedViews(this.options.overlayedViewStrategies ?? []);
15481
+ await comp.schemaFence.init();
14381
15482
  this.vaultCache.set(name, comp);
14382
15483
  return comp;
14383
15484
  }
@@ -14805,6 +15906,14 @@ var init_noydb = __esm({
14805
15906
  if (typeof arg === "function") {
14806
15907
  return this.txStrategy.runTransaction(this, arg);
14807
15908
  }
15909
+ if (typeof arg === "object" && arg !== null && arg.dryRun === true) {
15910
+ if (typeof maybeFn !== "function") {
15911
+ throw new ValidationError(
15912
+ "db.transaction({ dryRun: true }, fn) requires the callback as the second argument."
15913
+ );
15914
+ }
15915
+ return this.txStrategy.runDryRun(this, maybeFn);
15916
+ }
14808
15917
  if (typeof arg === "object" && arg !== null && arg.amendment === true) {
14809
15918
  if (typeof maybeFn !== "function") {
14810
15919
  throw new ValidationError(
@@ -14917,6 +16026,133 @@ var init_noydb = __esm({
14917
16026
  off(event, handler) {
14918
16027
  this.emitter.off(event, handler);
14919
16028
  }
16029
+ /**
16030
+ * Observable write-queue for this hub instance. Reflects outstanding
16031
+ * in-flight writes across all collections. See {@link WriteQueue}.
16032
+ *
16033
+ * @example
16034
+ * window.addEventListener('beforeunload', (e) => {
16035
+ * if (db.writeQueue.pending) { e.preventDefault(); e.returnValue = '' }
16036
+ * })
16037
+ */
16038
+ get writeQueue() {
16039
+ return this.writeQueueTracker;
16040
+ }
16041
+ /**
16042
+ * @internal Mutable tracker behind {@link writeQueue}. Threaded into
16043
+ * each Collection (via Vault) so `put`/`delete` can `track()` writes.
16044
+ * Not part of the public surface — consumers use `writeQueue`.
16045
+ */
16046
+ get _writeQueueTracker() {
16047
+ return this.writeQueueTracker;
16048
+ }
16049
+ /**
16050
+ * Register a hook that runs before each write (#230). Awaited; a throw
16051
+ * aborts the write. Returns an unsubscribe function.
16052
+ */
16053
+ onBeforeWrite(handler) {
16054
+ return this.writeHooks.onBeforeWrite(handler);
16055
+ }
16056
+ /**
16057
+ * Register a hook that runs after each committed write (#230). Awaited;
16058
+ * a handler error is warned, never rolled back. Returns an unsubscribe fn.
16059
+ */
16060
+ onAfterWrite(handler) {
16061
+ return this.writeHooks.onAfterWrite(handler);
16062
+ }
16063
+ /** Subscribe to cross-tab write conflicts (#228c). Returns an unsubscribe. */
16064
+ onWriteConflict(fn) {
16065
+ this.on("write:conflict", fn);
16066
+ return () => this.off("write:conflict", fn);
16067
+ }
16068
+ /**
16069
+ * Enable same-device multi-tab coordination (#228): primary/secondary
16070
+ * election + presence. Browser-only — a graceful no-op (role 'unknown')
16071
+ * when Web Locks / BroadcastChannel are unavailable and nothing is
16072
+ * injected. Idempotent; returns a disposer.
16073
+ */
16074
+ enableTabCoordination(opts = {}) {
16075
+ if (this.tabCoordinator) return { dispose: () => this.disableTabCoordination() };
16076
+ const lockManager = opts.lockManager ?? defaultLockManager();
16077
+ const channel = opts.channel ?? defaultChannel();
16078
+ const c = new TabCoordinator({
16079
+ ...opts,
16080
+ ...lockManager ? { lockManager } : {},
16081
+ ...channel ? { channel } : {},
16082
+ // We own the channel only when we created the default; never close a caller-injected one.
16083
+ closeChannelOnDispose: opts.channel === void 0 && channel !== void 0
16084
+ });
16085
+ this.tabCoordinator = c;
16086
+ c.start();
16087
+ if (opts.propagateWrites !== false) {
16088
+ const writeChannel = opts.writeChannel ?? defaultChannel("noydb:tab-writes");
16089
+ if (writeChannel) {
16090
+ const relay = new CrossTabWriteRelay({
16091
+ channel: writeChannel,
16092
+ writerId: c.tabId,
16093
+ subscribeAfterWrite: (h) => this.onAfterWrite(h),
16094
+ applyRemoteWrite: (vault, collection, docId, action) => this.#applyRemoteWrite(vault, collection, docId, action),
16095
+ reportConflict: (vault, collection, docId, action, baseV, v, ownV) => this.#reportWriteConflict(vault, collection, docId, action, baseV, v, ownV),
16096
+ // Own the channel only when we created the default (mirrors the presence channel).
16097
+ closeChannelOnDispose: opts.writeChannel === void 0 && writeChannel !== void 0
16098
+ });
16099
+ this.writeRelay = relay;
16100
+ relay.start();
16101
+ }
16102
+ }
16103
+ return { dispose: () => this.disableTabCoordination() };
16104
+ }
16105
+ #applyRemoteWrite(vaultName, collectionName, docId, action) {
16106
+ const v = this.vaultCache.get(vaultName);
16107
+ if (!v) return Promise.resolve();
16108
+ return v._applyRemoteWrite(collectionName, docId, action);
16109
+ }
16110
+ async #reportWriteConflict(vaultName, collectionName, docId, action, baseV, v, ownV) {
16111
+ const vault = this.vaultCache.get(vaultName);
16112
+ if (!vault) return;
16113
+ const cap = await vault._captureAndConverge(collectionName, docId, action, baseV);
16114
+ if (!cap) return;
16115
+ const conflict = {
16116
+ vault: vaultName,
16117
+ collection: collectionName,
16118
+ docId,
16119
+ local: cap.local,
16120
+ remote: cap.remote,
16121
+ base: cap.base,
16122
+ localVersion: ownV,
16123
+ remoteVersion: v,
16124
+ baseVersion: baseV
16125
+ };
16126
+ this.emitter.emit("write:conflict", conflict);
16127
+ }
16128
+ disableTabCoordination() {
16129
+ this.tabCoordinator?.dispose();
16130
+ this.tabCoordinator = void 0;
16131
+ this.writeRelay?.dispose();
16132
+ this.writeRelay = void 0;
16133
+ }
16134
+ get tabRole() {
16135
+ return this.tabCoordinator?.role ?? "unknown";
16136
+ }
16137
+ activeTabs() {
16138
+ return this.tabCoordinator?.activeTabs() ?? [];
16139
+ }
16140
+ onTabRoleChange(fn) {
16141
+ return this.tabCoordinator?.onTabRoleChange(fn) ?? (() => {
16142
+ });
16143
+ }
16144
+ onActiveTabsChange(fn) {
16145
+ return this.tabCoordinator?.onActiveTabsChange(fn) ?? (() => {
16146
+ });
16147
+ }
16148
+ /** @internal The write-hook registry, threaded into each Collection. */
16149
+ get _writeHooks() {
16150
+ return this.writeHooks;
16151
+ }
16152
+ /** @internal Stable per-instance id for schema-cutover coordination (#232). */
16153
+ get _clientId() {
16154
+ return this.clientId;
16155
+ }
14920
16156
  /**
14921
16157
  * Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
14922
16158
  * instance, sync engine, policy enforcer, and active-tier entry —
@@ -14943,6 +16179,7 @@ var init_noydb = __esm({
14943
16179
  this.syncEngines.delete(vault);
14944
16180
  this.policyEnforcers.get(vault)?.destroy();
14945
16181
  this.policyEnforcers.delete(vault);
16182
+ this.vaultCache.get(vault)?._stopFenceCoordination();
14946
16183
  this.keyringCache.delete(vault);
14947
16184
  this.vaultCache.delete(vault);
14948
16185
  this.activeTier.delete(vault);
@@ -14962,6 +16199,8 @@ var init_noydb = __esm({
14962
16199
  engine.stopAutoSync();
14963
16200
  }
14964
16201
  this.syncEngines.clear();
16202
+ for (const v of this.vaultCache.values()) v._stopFenceCoordination();
16203
+ this.disableTabCoordination();
14965
16204
  this.keyringCache.clear();
14966
16205
  this.vaultCache.clear();
14967
16206
  this.activeTier.clear();