@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
package/dist/index.cjs CHANGED
@@ -46,7 +46,7 @@ var init_types = __esm({
46
46
  });
47
47
 
48
48
  // src/errors.ts
49
- var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, ReadOnlyAtInstantError, ReadOnlyFrameError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, PeriodClosedError, RecordLockedError, FieldFrozenError, InvariantError, AmendmentForbiddenError, DirectoryDisabledError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, BundleVersionConflictError, NetworkError, NotFoundError, ValidationError, SchemaValidationError, GroupCardinalityError, IndexRequiredError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, DictKeyMissingError, DictKeyInUseError, MissingTranslationError, LocaleNotSpecifiedError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, AttestationError, SessionExpiredError, SessionNotFoundError, SessionPolicyError, JoinTooLargeError, DanglingReferenceError, FilenameSanitizationError, PathEscapeError, DerivationCycleError, DerivationDepthError, DerivationOutputUnknownError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, MaterializedViewConfigError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError;
49
+ var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, ReadOnlyAtInstantError, ReadOnlyFrameError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, PeriodClosedError, RecordLockedError, FieldFrozenError, InvariantError, AmendmentForbiddenError, DirectoryDisabledError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, BundleVersionConflictError, NetworkError, NotFoundError, ValidationError, SchemaValidationError, SchemaUpdateError, NonAdditiveSchemaChangeError, SchemaLockedError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, DictKeyMissingError, DictKeyInUseError, MissingTranslationError, LocaleNotSpecifiedError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, AttestationError, SessionExpiredError, SessionNotFoundError, SessionPolicyError, JoinTooLargeError, DanglingReferenceError, FilenameSanitizationError, PathEscapeError, DerivationCycleError, DerivationDepthError, DerivationOutputUnknownError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, MaterializedViewConfigError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError;
50
50
  var init_errors = __esm({
51
51
  "src/errors.ts"() {
52
52
  "use strict";
@@ -377,6 +377,42 @@ var init_errors = __esm({
377
377
  this.direction = direction;
378
378
  }
379
379
  };
380
+ SchemaUpdateError = class extends NoydbError {
381
+ constructor(code, message) {
382
+ super(code, message);
383
+ this.name = "SchemaUpdateError";
384
+ }
385
+ };
386
+ NonAdditiveSchemaChangeError = class extends SchemaUpdateError {
387
+ constructor(message) {
388
+ super("NON_ADDITIVE_SCHEMA_CHANGE", message);
389
+ this.name = "NonAdditiveSchemaChangeError";
390
+ }
391
+ };
392
+ SchemaLockedError = class extends SchemaUpdateError {
393
+ constructor(message) {
394
+ super("SCHEMA_LOCKED", message);
395
+ this.name = "SchemaLockedError";
396
+ }
397
+ };
398
+ SchemaFenceError = class extends SchemaUpdateError {
399
+ constructor(message) {
400
+ super("SCHEMA_FENCE", message);
401
+ this.name = "SchemaFenceError";
402
+ }
403
+ };
404
+ MigrationRequiredError = class extends SchemaUpdateError {
405
+ constructor(message) {
406
+ super("MIGRATION_REQUIRED", message);
407
+ this.name = "MigrationRequiredError";
408
+ }
409
+ };
410
+ QuiesceTimeoutError = class extends SchemaUpdateError {
411
+ constructor(message) {
412
+ super("QUIESCE_TIMEOUT", message);
413
+ this.name = "QuiesceTimeoutError";
414
+ }
415
+ };
380
416
  GroupCardinalityError = class extends NoydbError {
381
417
  /** The field being grouped on. */
382
418
  field;
@@ -3506,6 +3542,13 @@ var init_registry2 = __esm({
3506
3542
  guardsFor(collection) {
3507
3543
  return this._byCollection.get(collection) ?? [];
3508
3544
  }
3545
+ /** Per-collection guard counts, for introspection (#229). */
3546
+ summary() {
3547
+ return [...this._byCollection.entries()].map(([collection, guards]) => ({
3548
+ collection,
3549
+ count: guards.length
3550
+ }));
3551
+ }
3509
3552
  /**
3510
3553
  * Run every guard's `check` for this collection. First throw wins —
3511
3554
  * remaining guards are not invoked. Guards without a `check` skip.
@@ -3956,6 +3999,7 @@ __export(src_exports, {
3956
3999
  MaterializedViewTooLargeError: () => MaterializedViewTooLargeError,
3957
4000
  MemoryRecipientSealer: () => MemoryRecipientSealer,
3958
4001
  MemorySealingKeyProvider: () => MemorySealingKeyProvider,
4002
+ MigrationRequiredError: () => MigrationRequiredError,
3959
4003
  MissingTranslationError: () => MissingTranslationError,
3960
4004
  NOYDB_BACKUP_VERSION: () => NOYDB_BACKUP_VERSION,
3961
4005
  NOYDB_BUNDLE_FORMAT_VERSION: () => NOYDB_BUNDLE_FORMAT_VERSION,
@@ -3966,6 +4010,7 @@ __export(src_exports, {
3966
4010
  NOYDB_SYNC_VERSION: () => NOYDB_SYNC_VERSION,
3967
4011
  NetworkError: () => NetworkError,
3968
4012
  NoAccessError: () => NoAccessError,
4013
+ NonAdditiveSchemaChangeError: () => NonAdditiveSchemaChangeError,
3969
4014
  NotFoundError: () => NotFoundError,
3970
4015
  Noydb: () => Noydb,
3971
4016
  NoydbError: () => NoydbError,
@@ -3987,6 +4032,7 @@ __export(src_exports, {
3987
4032
  PrivilegeEscalationError: () => PrivilegeEscalationError,
3988
4033
  Query: () => Query,
3989
4034
  QuickUnlockStore: () => QuickUnlockStore,
4035
+ QuiesceTimeoutError: () => QuiesceTimeoutError,
3990
4036
  ReadOnlyAtInstantError: () => ReadOnlyAtInstantError,
3991
4037
  ReadOnlyError: () => ReadOnlyError,
3992
4038
  ReadOnlyFrameError: () => ReadOnlyFrameError,
@@ -4002,6 +4048,9 @@ __export(src_exports, {
4002
4048
  STRICT_POLICY: () => STRICT_POLICY,
4003
4049
  SYNC_CREDENTIALS_COLLECTION: () => SYNC_CREDENTIALS_COLLECTION,
4004
4050
  ScanBuilder: () => ScanBuilder,
4051
+ SchemaFenceError: () => SchemaFenceError,
4052
+ SchemaLockedError: () => SchemaLockedError,
4053
+ SchemaUpdateError: () => SchemaUpdateError,
4005
4054
  SchemaValidationError: () => SchemaValidationError,
4006
4055
  SessionExpiredError: () => SessionExpiredError,
4007
4056
  SessionNotFoundError: () => SessionNotFoundError,
@@ -4028,6 +4077,7 @@ __export(src_exports, {
4028
4077
  VaultInstant: () => VaultInstant,
4029
4078
  WeakPassphraseError: () => WeakPassphraseError,
4030
4079
  activeSessionCount: () => activeSessionCount,
4080
+ additiveOnly: () => additiveOnly,
4031
4081
  applyI18nLocale: () => applyI18nLocale,
4032
4082
  applyJoins: () => applyJoins,
4033
4083
  applyPatch: () => applyPatch,
@@ -4035,6 +4085,7 @@ __export(src_exports, {
4035
4085
  assertTierAccess: () => assertTierAccess,
4036
4086
  avg: () => avg,
4037
4087
  base64ToBuffer: () => base64ToBuffer,
4088
+ blindUpdate: () => blindUpdate,
4038
4089
  bufferToBase64: () => bufferToBase64,
4039
4090
  buildLiveQuery: () => buildLiveQuery,
4040
4091
  buildRecipientKeyringFile: () => buildRecipientKeyringFile,
@@ -4043,6 +4094,7 @@ __export(src_exports, {
4043
4094
  checkGate: () => checkGate,
4044
4095
  clearDevUnlock: () => clearDevUnlock,
4045
4096
  computePatch: () => computePatch,
4097
+ coordinatedCutover: () => coordinatedCutover,
4046
4098
  count: () => count,
4047
4099
  createBundleStore: () => createBundleStore,
4048
4100
  createEnforcer: () => createEnforcer,
@@ -4121,6 +4173,7 @@ __export(src_exports, {
4121
4173
  loadShamirRecoveryEntries: () => loadShamirRecoveryEntries,
4122
4174
  loadUserEnvelope: () => loadUserEnvelope,
4123
4175
  loadVaultPolicy: () => loadVaultPolicy,
4176
+ lockSchema: () => lockSchema,
4124
4177
  magicLinkGrantRecordId: () => magicLinkGrantRecordId,
4125
4178
  max: () => max,
4126
4179
  mergeCrdtStates: () => mergeCrdtStates,
@@ -5202,6 +5255,127 @@ function createBundleStore(factory) {
5202
5255
  return factory;
5203
5256
  }
5204
5257
 
5258
+ // src/persisted-schemas/canonicalize.ts
5259
+ function canonicalize(value) {
5260
+ if (value === null || typeof value !== "object") {
5261
+ return JSON.stringify(value);
5262
+ }
5263
+ if (Array.isArray(value)) {
5264
+ return "[" + value.map(canonicalize).join(",") + "]";
5265
+ }
5266
+ const obj = value;
5267
+ const keys = Object.keys(obj).sort();
5268
+ const parts = keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k]));
5269
+ return "{" + parts.join(",") + "}";
5270
+ }
5271
+
5272
+ // src/schema-update/delta.ts
5273
+ function computeSchemaDelta(stored, fresh, collection) {
5274
+ const a = stored;
5275
+ const b = fresh;
5276
+ const aProps = a.properties ?? {};
5277
+ const bProps = b.properties ?? {};
5278
+ const aReq = new Set(a.required ?? []);
5279
+ const bReq = new Set(b.required ?? []);
5280
+ const aKeys = Object.keys(aProps);
5281
+ const bKeys = Object.keys(bProps);
5282
+ const added = bKeys.filter((k) => !(k in aProps));
5283
+ const removed = aKeys.filter((k) => !(k in bProps));
5284
+ const changed = [];
5285
+ for (const k of bKeys) {
5286
+ if (!(k in aProps)) continue;
5287
+ const shapeChanged = canonicalize(aProps[k]) !== canonicalize(bProps[k]);
5288
+ const requiredChanged = aReq.has(k) !== bReq.has(k);
5289
+ if (shapeChanged || requiredChanged) {
5290
+ changed.push({ field: k, requiredChanged, shapeChanged });
5291
+ }
5292
+ }
5293
+ let kind;
5294
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
5295
+ kind = "none";
5296
+ } else if (removed.length === 0 && changed.length === 0 && added.every((k) => !bReq.has(k))) {
5297
+ kind = "additive";
5298
+ } else {
5299
+ kind = "non-additive";
5300
+ }
5301
+ return { collection, kind, added, removed, changed };
5302
+ }
5303
+
5304
+ // src/schema-update/dispatch.ts
5305
+ async function evaluateStrategies(delta, strategies, ctx) {
5306
+ for (const strategy of strategies) {
5307
+ const decision = await strategy.onSchemaDelta(delta, ctx);
5308
+ if (decision.action !== "allow") return decision;
5309
+ }
5310
+ return { action: "allow" };
5311
+ }
5312
+
5313
+ // src/schema-update/strategies.ts
5314
+ init_errors();
5315
+ function blindUpdate() {
5316
+ return { name: "blindUpdate", onSchemaDelta: () => ({ action: "allow" }) };
5317
+ }
5318
+ function additiveOnly() {
5319
+ return {
5320
+ name: "additiveOnly",
5321
+ onSchemaDelta(delta) {
5322
+ if (delta.kind === "non-additive") {
5323
+ return {
5324
+ action: "reject",
5325
+ error: new NonAdditiveSchemaChangeError(
5326
+ `Non-additive schema change to "${delta.collection}" (added: [${delta.added.join(", ")}], removed: [${delta.removed.join(", ")}], changed: [${delta.changed.map((c) => c.field).join(", ")}]). Register a coordinatedCutover() strategy to migrate, or revert the change.`
5327
+ )
5328
+ };
5329
+ }
5330
+ return { action: "allow" };
5331
+ }
5332
+ };
5333
+ }
5334
+ function lockSchema(opts) {
5335
+ const fields = opts?.fields;
5336
+ return {
5337
+ name: "lockSchema",
5338
+ onSchemaDelta(delta) {
5339
+ if (delta.kind === "none") return { action: "allow" };
5340
+ const touched = fields ? [...delta.added, ...delta.removed, ...delta.changed.map((c) => c.field)].filter((f) => fields.includes(f)) : ["<any>"];
5341
+ if (touched.length === 0) return { action: "allow" };
5342
+ return {
5343
+ action: "reject",
5344
+ error: new SchemaLockedError(
5345
+ `Schema for "${delta.collection}" is locked` + (fields ? ` on fields [${fields.join(", ")}] (touched: [${touched.join(", ")}])` : "") + `; the change was refused.`
5346
+ )
5347
+ };
5348
+ }
5349
+ };
5350
+ }
5351
+
5352
+ // src/schema-update/cutover.ts
5353
+ function coordinatedCutover(opts) {
5354
+ return {
5355
+ name: "coordinatedCutover",
5356
+ onSchemaDelta(delta) {
5357
+ if (delta.kind === "non-additive") {
5358
+ return { action: "cutover", transform: opts.transform };
5359
+ }
5360
+ return { action: "allow" };
5361
+ }
5362
+ };
5363
+ }
5364
+
5365
+ // src/schema-update/gate.ts
5366
+ var SchemaUpdateGate = class {
5367
+ #decision;
5368
+ constructor(decision) {
5369
+ this.#decision = decision.catch(() => null);
5370
+ }
5371
+ async assertWritable() {
5372
+ const decision = await this.#decision;
5373
+ if (decision && decision.action === "reject") {
5374
+ throw decision.error;
5375
+ }
5376
+ }
5377
+ };
5378
+
5205
5379
  // src/store/sync-policy.ts
5206
5380
  var INDEXED_STORE_POLICY = {
5207
5381
  push: { mode: "on-change", minIntervalMs: 0, onUnload: true },
@@ -5776,8 +5950,8 @@ function withRetry(opts = {}) {
5776
5950
  } catch (err) {
5777
5951
  lastError = err;
5778
5952
  if (attempt >= maxRetries || !shouldRetry(err)) throw err;
5779
- const delay = backoffMs * Math.pow(2, attempt) * (1 + Math.random() * jitter);
5780
- await new Promise((r) => setTimeout(r, delay));
5953
+ const delay2 = backoffMs * Math.pow(2, attempt) * (1 + Math.random() * jitter);
5954
+ await new Promise((r) => setTimeout(r, delay2));
5781
5955
  }
5782
5956
  }
5783
5957
  throw lastError;
@@ -6824,20 +6998,6 @@ function formatPath(path) {
6824
6998
  ).join(".");
6825
6999
  }
6826
7000
 
6827
- // src/persisted-schemas/canonicalize.ts
6828
- function canonicalize(value) {
6829
- if (value === null || typeof value !== "object") {
6830
- return JSON.stringify(value);
6831
- }
6832
- if (Array.isArray(value)) {
6833
- return "[" + value.map(canonicalize).join(",") + "]";
6834
- }
6835
- const obj = value;
6836
- const keys = Object.keys(obj).sort();
6837
- const parts = keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k]));
6838
- return "{" + parts.join(",") + "}";
6839
- }
6840
-
6841
7001
  // src/persisted-schemas/derive.ts
6842
7002
  init_crypto();
6843
7003
  function isZodSchema(value) {
@@ -6918,10 +7078,22 @@ async function persistSchemaIfNeeded(opts) {
6918
7078
  const fresh = await derivePersistedSchema(opts.validator);
6919
7079
  const stored = await loadPersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek);
6920
7080
  if (stored && isEquivalent(stored, fresh)) {
6921
- return { written: false, skipped: true, envelope: stored };
7081
+ return { written: false, skipped: true, envelope: stored, decision: { action: "allow" } };
7082
+ }
7083
+ let decision = { action: "allow" };
7084
+ const strategies = opts.strategies ?? [];
7085
+ if (stored && strategies.length > 0 && stored.kind === fresh.kind && isPlainObject(stored.jsonSchema) && isPlainObject(fresh.jsonSchema)) {
7086
+ const delta = computeSchemaDelta(stored.jsonSchema, fresh.jsonSchema, opts.collectionName);
7087
+ decision = await evaluateStrategies(delta, strategies, { collection: opts.collectionName });
7088
+ }
7089
+ if (decision.action !== "allow") {
7090
+ return { written: false, skipped: false, envelope: stored ?? fresh, decision };
6922
7091
  }
6923
7092
  await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, fresh);
6924
- return { written: true, skipped: false, envelope: fresh };
7093
+ return { written: true, skipped: false, envelope: fresh, decision };
7094
+ }
7095
+ function isPlainObject(v) {
7096
+ return typeof v === "object" && v !== null && !Array.isArray(v);
6925
7097
  }
6926
7098
  function isEquivalent(a, b) {
6927
7099
  if (a.kind !== b.kind) return false;
@@ -7063,7 +7235,7 @@ var CollectionInstant = class {
7063
7235
  if (e.collection !== this.name || e.id !== id) continue;
7064
7236
  if (e.ts > this.targetTs) break;
7065
7237
  if (e.op === "amendment" || e.op === "lifecycle") continue;
7066
- latest = { op: e.op, version: e.version };
7238
+ latest = { op: e.op === "migration" ? "put" : e.op, version: e.version };
7067
7239
  }
7068
7240
  if (!latest) return null;
7069
7241
  if (latest.op === "delete") return null;
@@ -9016,7 +9188,7 @@ var UserApi = class {
9016
9188
  }
9017
9189
  };
9018
9190
  function deepMerge(source, patch) {
9019
- if (!isPlainObject(source) || !isPlainObject(patch)) {
9191
+ if (!isPlainObject2(source) || !isPlainObject2(patch)) {
9020
9192
  return patch;
9021
9193
  }
9022
9194
  const out = { ...source };
@@ -9029,8 +9201,8 @@ function deepMerge(source, patch) {
9029
9201
  continue;
9030
9202
  }
9031
9203
  const sourceVal = source[key];
9032
- if (isPlainObject(patchVal)) {
9033
- const recurseSource = isPlainObject(sourceVal) ? sourceVal : {};
9204
+ if (isPlainObject2(patchVal)) {
9205
+ const recurseSource = isPlainObject2(sourceVal) ? sourceVal : {};
9034
9206
  out[key] = deepMerge(recurseSource, patchVal);
9035
9207
  } else {
9036
9208
  out[key] = patchVal;
@@ -9038,7 +9210,7 @@ function deepMerge(source, patch) {
9038
9210
  }
9039
9211
  return out;
9040
9212
  }
9041
- function isPlainObject(x) {
9213
+ function isPlainObject2(x) {
9042
9214
  if (x === null || typeof x !== "object") return false;
9043
9215
  if (Array.isArray(x)) return false;
9044
9216
  const proto = Object.getPrototypeOf(x);
@@ -11521,7 +11693,10 @@ var NO_BLOBS = {
11521
11693
 
11522
11694
  // src/tx/transaction.ts
11523
11695
  init_errors();
11696
+ init_ulid();
11524
11697
  var TxContext = class {
11698
+ /** Stable id for this transaction; shared by all writes it performs (#230). */
11699
+ txId = generateULID();
11525
11700
  /** @internal */
11526
11701
  _ops = [];
11527
11702
  /**
@@ -11886,6 +12061,11 @@ var Collection = class {
11886
12061
  keyring;
11887
12062
  encrypted;
11888
12063
  emitter;
12064
+ writeQueue;
12065
+ schemaUpdateGate;
12066
+ schemaFence;
12067
+ writeHooks;
12068
+ activeTxId;
11889
12069
  getDEK;
11890
12070
  onDirty;
11891
12071
  historyConfig;
@@ -12160,6 +12340,11 @@ var Collection = class {
12160
12340
  this.keyring = opts.keyring;
12161
12341
  this.encrypted = opts.encrypted;
12162
12342
  this.emitter = opts.emitter;
12343
+ this.writeQueue = opts.writeQueue;
12344
+ this.schemaUpdateGate = opts.schemaUpdateGate;
12345
+ this.schemaFence = opts.schemaFence;
12346
+ this.writeHooks = opts.writeHooks;
12347
+ this.activeTxId = opts.activeTxId;
12163
12348
  this.blobStrategy = opts.blobStrategy ?? NO_BLOBS;
12164
12349
  this.aggregateStrategy = opts.aggregateStrategy ?? NO_AGGREGATE;
12165
12350
  this.crdtStrategy = opts.crdtStrategy ?? NO_CRDT;
@@ -12385,7 +12570,8 @@ var Collection = class {
12385
12570
  return this.syncStrategy.buildPresence(presenceOpts);
12386
12571
  }
12387
12572
  /**
12388
- * Create or update a record.
12573
+ * Create or update a record. Runs inside the hub's write-queue tracker
12574
+ * (#227) so `hub.writeQueue.pending` reflects this write.
12389
12575
  *
12390
12576
  * @param id Record identifier.
12391
12577
  * @param record The record body (validated by the collection's schema
@@ -12396,6 +12582,59 @@ var Collection = class {
12396
12582
  * `entries.filter(e => e.reason?.startsWith('import:'))`.
12397
12583
  */
12398
12584
  async put(id, record, options) {
12585
+ await this.schemaUpdateGate?.assertWritable();
12586
+ await this.schemaFence?.assertWritable(this.name);
12587
+ let event;
12588
+ if (this.#hooksActive()) {
12589
+ const prior = await this.#priorForHook(id);
12590
+ event = {
12591
+ op: prior.record === null ? "create" : "update",
12592
+ vault: this.vault,
12593
+ collection: this.name,
12594
+ docId: id,
12595
+ before: prior.record,
12596
+ after: record,
12597
+ userId: this.keyring.userId,
12598
+ timestamp: Date.now(),
12599
+ txId: this.#txIdForHook(),
12600
+ baseVersion: prior.version,
12601
+ version: prior.version + 1
12602
+ };
12603
+ await this.writeHooks.runBefore(event);
12604
+ }
12605
+ if (this.writeQueue) await this.writeQueue.track(() => this.putInternal(id, record, options));
12606
+ else await this.putInternal(id, record, options);
12607
+ if (event) await this.writeHooks.runAfter(event);
12608
+ }
12609
+ /** @internal #230 — true when hooks should fire for this write (handlers exist, not re-entrant). */
12610
+ #hooksActive() {
12611
+ return this.writeHooks !== void 0 && this.writeHooks.hasHandlers && !this.writeHooks.suppressed;
12612
+ }
12613
+ /**
12614
+ * @internal #230/#228c — resolve the prior record for a hook's `before` and
12615
+ * its version. Critically, this uses the SAME basis `putInternal` writes from
12616
+ * (the in-memory cache in eager mode; lru-then-adapter in lazy) — NOT a fresh
12617
+ * store read — so `baseVersion`/`version` match the version actually written.
12618
+ * A separate store read would diverge once another tab has advanced the shared
12619
+ * store past this tab's cache, breaking #228c conflict detection.
12620
+ */
12621
+ async #priorForHook(id) {
12622
+ if (this.lazy && this.lru) {
12623
+ const cached2 = this.lru.get(id);
12624
+ if (cached2) return { record: cached2.record, version: cached2.version };
12625
+ const env = await this.adapter.get(this.vault, this.name, id);
12626
+ if (!env) return { record: null, version: 0 };
12627
+ return { record: await this.decryptRecord(env, { skipValidation: true }), version: env._v };
12628
+ }
12629
+ await this.ensureHydrated();
12630
+ const cached = this.cache.get(id);
12631
+ return cached ? { record: cached.record, version: cached.version } : { record: null, version: 0 };
12632
+ }
12633
+ #txIdForHook() {
12634
+ return this.activeTxId?.() ?? generateULID();
12635
+ }
12636
+ /** @internal Untracked put body — call {@link put}, not this. */
12637
+ async putInternal(id, record, options) {
12399
12638
  if (!hasWritePermission(this.keyring, this.name)) {
12400
12639
  throw new ReadOnlyError();
12401
12640
  }
@@ -12772,8 +13011,71 @@ var Collection = class {
12772
13011
  }
12773
13012
  }
12774
13013
  }
12775
- /** Delete a record by ID. */
13014
+ /**
13015
+ * Delete a record by ID. Runs inside the hub's write-queue tracker
13016
+ * (#227) so `hub.writeQueue.pending` reflects this write.
13017
+ */
12776
13018
  async delete(id) {
13019
+ await this.schemaUpdateGate?.assertWritable();
13020
+ await this.schemaFence?.assertWritable(this.name);
13021
+ let event;
13022
+ if (this.#hooksActive()) {
13023
+ const prior = await this.#priorForHook(id);
13024
+ event = {
13025
+ op: "delete",
13026
+ vault: this.vault,
13027
+ collection: this.name,
13028
+ docId: id,
13029
+ before: prior.record,
13030
+ after: null,
13031
+ userId: this.keyring.userId,
13032
+ timestamp: Date.now(),
13033
+ txId: this.#txIdForHook(),
13034
+ baseVersion: prior.version,
13035
+ version: prior.version + 1
13036
+ };
13037
+ await this.writeHooks.runBefore(event);
13038
+ }
13039
+ if (this.writeQueue) await this.writeQueue.track(() => this.deleteInternal(id));
13040
+ else await this.deleteInternal(id);
13041
+ if (event) await this.writeHooks.runAfter(event);
13042
+ }
13043
+ /**
13044
+ * @internal #232 — bulk-rewrite every record through a cutover transform.
13045
+ * Raw adapter path (bypasses the write gate + guards — the transform is
13046
+ * trusted and runs only during the `migrating` phase). Bumps each
13047
+ * record's `_v` and appends a ledger `op:'migration'` entry.
13048
+ */
13049
+ async _applyCutoverTransform(transform) {
13050
+ const ids = await this.adapter.list(this.vault, this.name);
13051
+ let count2 = 0;
13052
+ for (const id of ids) {
13053
+ const env = await this.adapter.get(this.vault, this.name, id);
13054
+ if (!env) continue;
13055
+ const record = await this.decryptRecord(env, { skipValidation: true });
13056
+ const next = transform(record);
13057
+ const nextVersion = (env._v ?? 0) + 1;
13058
+ const newEnv = await this.encryptRecord(next, nextVersion);
13059
+ await this.adapter.put(this.vault, this.name, id, newEnv);
13060
+ await this._invalidateCacheEntry(id);
13061
+ if (this.ledger) {
13062
+ await this.ledger.append({
13063
+ op: "migration",
13064
+ collection: this.name,
13065
+ id,
13066
+ version: nextVersion,
13067
+ actor: this.keyring.userId,
13068
+ payloadHash: "",
13069
+ reason: "schema:coordinated-cutover"
13070
+ }).catch(() => {
13071
+ });
13072
+ }
13073
+ count2++;
13074
+ }
13075
+ return count2;
13076
+ }
13077
+ /** @internal Untracked delete body — call {@link delete}, not this. */
13078
+ async deleteInternal(id) {
12777
13079
  await this._doDelete(id, false);
12778
13080
  }
12779
13081
  /**
@@ -13596,6 +13898,21 @@ var Collection = class {
13596
13898
  this.cache.set(id, { record, version: envelope._v });
13597
13899
  this.indexes?.upsert(id, record, previous ? previous.record : null);
13598
13900
  }
13901
+ /**
13902
+ * #228b — apply a peer tab's committed write to THIS tab's in-memory view:
13903
+ * re-read the (already-persisted) envelope from the shared store + refresh
13904
+ * cache/indexes, then emit a `change` event so reactive consumers re-render.
13905
+ * Never writes to the store and never fires write hooks, so it cannot loop.
13906
+ */
13907
+ async _applyRemoteChange(id, action) {
13908
+ await this._invalidateCacheEntry(id);
13909
+ this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action });
13910
+ }
13911
+ /** @internal #228c — the current in-memory record without a store read (for conflict capture). */
13912
+ _peekCached(id) {
13913
+ const entry = this.lazy && this.lru ? this.lru.get(id) : this.cache.get(id);
13914
+ return entry ? entry.record : null;
13915
+ }
13599
13916
  async ensureHydrated() {
13600
13917
  if (this.hydrated) return;
13601
13918
  const ids = await this.adapter.list(this.vault, this.name);
@@ -15425,6 +15742,245 @@ function isMagicLinkGrantExpired(payload, now = /* @__PURE__ */ new Date()) {
15425
15742
  return payload.until <= now.toISOString();
15426
15743
  }
15427
15744
 
15745
+ // src/schema-update/fence.ts
15746
+ init_types();
15747
+ var FENCE_RECORD_ID = "schema-fence";
15748
+ var META_COLLECTION3 = "_meta";
15749
+ var DEFAULT_FENCE = { currentSchemaVersion: 0, fenceState: "normal" };
15750
+ async function loadFence(store, vault) {
15751
+ const envelope = await store.get(vault, META_COLLECTION3, FENCE_RECORD_ID);
15752
+ if (!envelope) return DEFAULT_FENCE;
15753
+ try {
15754
+ const parsed = JSON.parse(envelope._data);
15755
+ if (!isFenceDoc(parsed)) return DEFAULT_FENCE;
15756
+ return parsed;
15757
+ } catch {
15758
+ return DEFAULT_FENCE;
15759
+ }
15760
+ }
15761
+ async function saveFence(store, vault, fence) {
15762
+ const envelope = {
15763
+ _noydb: NOYDB_FORMAT_VERSION,
15764
+ _v: 1,
15765
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
15766
+ _iv: "",
15767
+ _data: JSON.stringify(fence)
15768
+ };
15769
+ await store.put(vault, META_COLLECTION3, FENCE_RECORD_ID, envelope);
15770
+ }
15771
+ function isFenceDoc(x) {
15772
+ if (x === null || typeof x !== "object") return false;
15773
+ const o = x;
15774
+ return typeof o["currentSchemaVersion"] === "number" && (o["fenceState"] === "normal" || o["fenceState"] === "draining" || o["fenceState"] === "migrating" || o["fenceState"] === "complete");
15775
+ }
15776
+
15777
+ // src/schema-update/fence-controller.ts
15778
+ init_errors();
15779
+
15780
+ // src/schema-update/client-registry.ts
15781
+ init_types();
15782
+ var META_COLLECTION4 = "_meta";
15783
+ var CLIENT_PREFIX = "schema-fence:client:";
15784
+ async function writeClientDoc(store, vault, clientId, doc) {
15785
+ const envelope = {
15786
+ _noydb: NOYDB_FORMAT_VERSION,
15787
+ _v: 1,
15788
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
15789
+ _iv: "",
15790
+ _data: JSON.stringify({ clientId, ...doc })
15791
+ };
15792
+ await store.put(vault, META_COLLECTION4, `${CLIENT_PREFIX}${clientId}`, envelope);
15793
+ }
15794
+ async function listClientDocs(store, vault) {
15795
+ const ids = await store.list(vault, META_COLLECTION4);
15796
+ const out = [];
15797
+ for (const id of ids) {
15798
+ if (!id.startsWith(CLIENT_PREFIX)) continue;
15799
+ const env = await store.get(vault, META_COLLECTION4, id);
15800
+ if (!env) continue;
15801
+ try {
15802
+ const parsed = JSON.parse(env._data);
15803
+ if (isClientDoc(parsed)) out.push(parsed);
15804
+ } catch {
15805
+ }
15806
+ }
15807
+ return out;
15808
+ }
15809
+ async function activeQuiesced(store, vault, opts) {
15810
+ const docs = await listClientDocs(store, vault);
15811
+ const active = docs.filter(
15812
+ (d) => d.lastSeen >= opts.now - opts.staleMs && d.clientId !== opts.excludeClientId
15813
+ );
15814
+ return active.every((d) => d.quiescedAtVersion === opts.generation);
15815
+ }
15816
+ function isClientDoc(x) {
15817
+ if (x === null || typeof x !== "object") return false;
15818
+ const o = x;
15819
+ return typeof o["clientId"] === "string" && typeof o["lastSeen"] === "number" && (o["quiescedAtVersion"] === null || typeof o["quiescedAtVersion"] === "number");
15820
+ }
15821
+
15822
+ // src/schema-update/fence-controller.ts
15823
+ var SchemaFenceController = class {
15824
+ #store;
15825
+ #vault;
15826
+ #onFlush;
15827
+ #clientId;
15828
+ #now;
15829
+ #staleMs;
15830
+ #quiesceTimeoutMs;
15831
+ #emit;
15832
+ #snapshot = 0;
15833
+ #pending = /* @__PURE__ */ new Map();
15834
+ constructor(opts) {
15835
+ this.#store = opts.store;
15836
+ this.#vault = opts.vault;
15837
+ this.#onFlush = opts.onFlush;
15838
+ this.#clientId = opts.clientId ?? "migrator";
15839
+ this.#now = opts.now ?? (() => Date.now());
15840
+ this.#staleMs = opts.staleMs ?? 3e4;
15841
+ this.#quiesceTimeoutMs = opts.quiesceTimeoutMs ?? 6e4;
15842
+ this.#emit = opts.emit ?? (() => {
15843
+ });
15844
+ }
15845
+ /** Capture the generation snapshot at vault-open. */
15846
+ async init() {
15847
+ this.#snapshot = (await loadFence(this.#store, this.#vault)).currentSchemaVersion;
15848
+ }
15849
+ /** Record a per-collection pending cutover (from a registration `cutover` decision). */
15850
+ registerPendingCutover(collection, transform) {
15851
+ this.#pending.set(collection, transform);
15852
+ }
15853
+ /** Write-path gate. Throws when behind, fenced, or this collection is cutover-pending. */
15854
+ async assertWritable(collection) {
15855
+ const fence = await loadFence(this.#store, this.#vault);
15856
+ if (fence.currentSchemaVersion > this.#snapshot) {
15857
+ throw new MigrationRequiredError(
15858
+ `Vault "${this.#vault}" advanced to schema generation ${fence.currentSchemaVersion} (this client opened at ${this.#snapshot}). Reload to continue.`
15859
+ );
15860
+ }
15861
+ if (fence.fenceState === "draining" || fence.fenceState === "migrating") {
15862
+ throw new SchemaFenceError(`Vault "${this.#vault}" is mid-cutover (${fence.fenceState}); writes are paused.`);
15863
+ }
15864
+ if (this.#pending.has(collection)) {
15865
+ throw new SchemaFenceError(
15866
+ `Collection "${collection}" has a pending schema cutover; run vault.runSchemaCutover() before writing.`
15867
+ );
15868
+ }
15869
+ }
15870
+ /**
15871
+ * Admin trigger. Drain → wait for the active set to quiesce (or time out)
15872
+ * → migrate each pending transform → bump → complete → normal. The
15873
+ * migrator excludes itself from the barrier (it drained synchronously
15874
+ * here). `onPoll` (tests) advances other clients between barrier checks;
15875
+ * production falls back to a short real delay.
15876
+ */
15877
+ async runCutover(run, opts) {
15878
+ if (this.#pending.size === 0) return { migrated: 0 };
15879
+ const base = await loadFence(this.#store, this.#vault);
15880
+ const generation = base.currentSchemaVersion;
15881
+ await this.#setState(generation, "draining");
15882
+ await this.#onFlush();
15883
+ const deadline = this.#now() + this.#quiesceTimeoutMs;
15884
+ while (!await activeQuiesced(this.#store, this.#vault, {
15885
+ generation,
15886
+ now: this.#now(),
15887
+ staleMs: this.#staleMs,
15888
+ excludeClientId: this.#clientId
15889
+ })) {
15890
+ if (this.#now() >= deadline) {
15891
+ throw new QuiesceTimeoutError(
15892
+ `Cutover on "${this.#vault}" timed out waiting for clients to quiesce at generation ${generation}.`
15893
+ );
15894
+ }
15895
+ await (opts?.onPoll ? opts.onPoll() : delay(50));
15896
+ }
15897
+ await this.#setState(generation, "migrating");
15898
+ let migrated = 0;
15899
+ for (const [collection, transform] of this.#pending) {
15900
+ await run(collection, transform);
15901
+ migrated++;
15902
+ }
15903
+ const nextVersion = generation + 1;
15904
+ await this.#setState(nextVersion, "complete");
15905
+ this.#pending.clear();
15906
+ await this.#setState(nextVersion, "normal");
15907
+ this.#snapshot = nextVersion;
15908
+ return { migrated };
15909
+ }
15910
+ /** Recover a stuck drain: reset fenceState to normal at the current version (no bump). */
15911
+ async abort() {
15912
+ const fence = await loadFence(this.#store, this.#vault);
15913
+ await this.#setState(fence.currentSchemaVersion, "normal");
15914
+ }
15915
+ async #setState(currentSchemaVersion, fenceState) {
15916
+ await saveFence(this.#store, this.#vault, { currentSchemaVersion, fenceState });
15917
+ this.#emit({ currentSchemaVersion, fenceState });
15918
+ }
15919
+ };
15920
+ function delay(ms) {
15921
+ return new Promise((resolve) => setTimeout(resolve, ms));
15922
+ }
15923
+
15924
+ // src/schema-update/fence-watcher.ts
15925
+ var FenceWatcher = class {
15926
+ #store;
15927
+ #vault;
15928
+ #clientId;
15929
+ #onFlush;
15930
+ #now;
15931
+ #emit;
15932
+ #lastState = null;
15933
+ #quiescedAtVersion = null;
15934
+ #timer;
15935
+ constructor(opts) {
15936
+ this.#store = opts.store;
15937
+ this.#vault = opts.vault;
15938
+ this.#clientId = opts.clientId;
15939
+ this.#onFlush = opts.onFlush;
15940
+ this.#now = opts.now ?? (() => Date.now());
15941
+ this.#emit = opts.emit ?? (() => {
15942
+ });
15943
+ }
15944
+ /** Publish liveness (and the current ack) without changing quiesce state. */
15945
+ async beat() {
15946
+ await writeClientDoc(this.#store, this.#vault, this.#clientId, {
15947
+ lastSeen: this.#now(),
15948
+ quiescedAtVersion: this.#quiescedAtVersion
15949
+ });
15950
+ }
15951
+ /** Poll the fence; quiesce on draining; emit on transitions. */
15952
+ async check() {
15953
+ const fence = await loadFence(this.#store, this.#vault);
15954
+ if (fence.fenceState !== this.#lastState) {
15955
+ this.#lastState = fence.fenceState;
15956
+ this.#emit({ currentSchemaVersion: fence.currentSchemaVersion, fenceState: fence.fenceState });
15957
+ }
15958
+ if (fence.fenceState === "draining" && this.#quiescedAtVersion !== fence.currentSchemaVersion) {
15959
+ await this.#onFlush();
15960
+ this.#quiescedAtVersion = fence.currentSchemaVersion;
15961
+ await this.beat();
15962
+ }
15963
+ if (fence.fenceState === "normal") {
15964
+ this.#quiescedAtVersion = null;
15965
+ }
15966
+ }
15967
+ start(intervalMs) {
15968
+ if (this.#timer) return;
15969
+ this.#timer = setInterval(() => {
15970
+ void this.beat();
15971
+ void this.check();
15972
+ }, intervalMs);
15973
+ const timer = this.#timer;
15974
+ if (typeof timer.unref === "function") timer.unref();
15975
+ }
15976
+ stop() {
15977
+ if (this.#timer) {
15978
+ clearInterval(this.#timer);
15979
+ this.#timer = void 0;
15980
+ }
15981
+ }
15982
+ };
15983
+
15428
15984
  // src/introspection/fields.ts
15429
15985
  function jsonSchemaType(node) {
15430
15986
  if (Array.isArray(node.type)) {
@@ -15771,6 +16327,13 @@ var Vault = class {
15771
16327
  */
15772
16328
  reloadKeyring;
15773
16329
  collectionCache = /* @__PURE__ */ new Map();
16330
+ /** #232 — vault-level schema cutover fence/controller. */
16331
+ schemaFence;
16332
+ /** #232 — per-client heartbeat/watcher; started lazily on cutover registration. */
16333
+ #fenceWatcher;
16334
+ #fenceCoordinationStarted = false;
16335
+ /** #229 — per-collection registered schema-update strategy names. */
16336
+ #schemaUpdateNames = /* @__PURE__ */ new Map();
15774
16337
  /**
15775
16338
  * per-collection `blobFields` retention/TTL config.
15776
16339
  * Populated on `collection({ blobFields })` and read by
@@ -15886,6 +16449,13 @@ var Vault = class {
15886
16449
  this.noydb = opts.noydb;
15887
16450
  this.keyring = opts.keyring;
15888
16451
  this.encrypted = opts.encrypted;
16452
+ this.schemaFence = new SchemaFenceController({
16453
+ store: this.adapter,
16454
+ vault: this.name,
16455
+ onFlush: () => this.noydb._writeQueueTracker.onFlush(),
16456
+ clientId: this.noydb._clientId,
16457
+ emit: (e) => this.emitter.emit("schema:fence-changed", { vault: this.name, ...e })
16458
+ });
15889
16459
  this.emitter = opts.emitter;
15890
16460
  this.onDirty = opts.onDirty;
15891
16461
  this.onRegisterConflictResolver = opts.onRegisterConflictResolver;
@@ -15994,6 +16564,35 @@ var Vault = class {
15994
16564
  }
15995
16565
  this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
15996
16566
  }
16567
+ if ((options?.schemaUpdate?.length ?? 0) > 0) {
16568
+ this.#schemaUpdateNames.set(collectionName, (options.schemaUpdate ?? []).map((s) => s.name));
16569
+ }
16570
+ let schemaUpdateGate;
16571
+ if (options?.persistJsonSchema === true && options.schema !== void 0 && (options.schemaUpdate?.length ?? 0) > 0) {
16572
+ const validator = options.schema;
16573
+ const strategies = options.schemaUpdate ?? [];
16574
+ const work = (async () => {
16575
+ const dek = await this.getDEK(collectionName);
16576
+ const result = await persistSchemaIfNeeded({
16577
+ store: this.adapter,
16578
+ vault: this.name,
16579
+ collectionName,
16580
+ validator,
16581
+ dek,
16582
+ strategies
16583
+ });
16584
+ const decision = result.decision ?? { action: "allow" };
16585
+ if (decision.action === "cutover") {
16586
+ this.schemaFence.registerPendingCutover(collectionName, decision.transform);
16587
+ this._ensureFenceCoordination();
16588
+ }
16589
+ return decision;
16590
+ })();
16591
+ this._pendingSchemaWrites.push(work.then(() => {
16592
+ }, () => {
16593
+ }));
16594
+ schemaUpdateGate = new SchemaUpdateGate(work);
16595
+ }
15997
16596
  const collOpts = {
15998
16597
  adapter: this.adapter,
15999
16598
  vault: this.name,
@@ -16001,6 +16600,11 @@ var Vault = class {
16001
16600
  keyring: this.keyring,
16002
16601
  encrypted: this.encrypted,
16003
16602
  emitter: this.emitter,
16603
+ writeQueue: this.noydb._writeQueueTracker,
16604
+ writeHooks: this.noydb._writeHooks,
16605
+ activeTxId: () => this.noydb._activeTxContextOrNull?.txId ?? null,
16606
+ schemaUpdateGate,
16607
+ schemaFence: this.schemaFence,
16004
16608
  getDEK: this.getDEK,
16005
16609
  onDirty: this.onDirty,
16006
16610
  historyConfig: this.historyConfig,
@@ -16087,7 +16691,7 @@ var Vault = class {
16087
16691
  }
16088
16692
  coll = new Collection(collOpts);
16089
16693
  this.collectionCache.set(collectionName, coll);
16090
- if (options?.persistJsonSchema === true && options.schema !== void 0) {
16694
+ if (options?.persistJsonSchema === true && options.schema !== void 0 && (options.schemaUpdate?.length ?? 0) === 0) {
16091
16695
  const validator = options.schema;
16092
16696
  const work = (async () => {
16093
16697
  try {
@@ -16120,6 +16724,87 @@ var Vault = class {
16120
16724
  this._pendingSchemaWrites = [];
16121
16725
  await Promise.allSettled(pending);
16122
16726
  }
16727
+ /**
16728
+ * Run a coordinated schema cutover (#232). Drains pending writes, waits
16729
+ * for the active client set to quiesce (the ack-barrier), applies every
16730
+ * pending collection transform in bulk, bumps the vault schema generation,
16731
+ * and clears the fence. Returns the count of collections migrated.
16732
+ * `opts.onPoll` (tests) advances other clients between barrier checks.
16733
+ */
16734
+ async runSchemaCutover(opts) {
16735
+ return this.schemaFence.runCutover(
16736
+ (collectionName, transform) => this.#runCutoverTransform(collectionName, transform),
16737
+ opts
16738
+ );
16739
+ }
16740
+ async #runCutoverTransform(collectionName, transform) {
16741
+ const coll = this.collectionCache.get(collectionName);
16742
+ if (!coll) return;
16743
+ await coll._applyCutoverTransform(transform);
16744
+ }
16745
+ /**
16746
+ * #228b — refresh a loaded collection's view of one document from a peer
16747
+ * tab's broadcast. No-op when the collection isn't loaded in this tab
16748
+ * (it will read fresh on next open). Mirrors #runCutoverTransform's guard.
16749
+ */
16750
+ async _applyRemoteWrite(collectionName, docId, action) {
16751
+ const coll = this.collectionCache.get(collectionName);
16752
+ if (!coll) return;
16753
+ await coll._applyRemoteChange(docId, action);
16754
+ }
16755
+ /**
16756
+ * #228c — for a detected conflict: capture this tab's clobbered record,
16757
+ * read the common ancestor from history, converge the cache to the store's
16758
+ * authoritative value (the (b) re-read), and return all three for the
16759
+ * WriteConflict payload. Returns null when the collection isn't loaded.
16760
+ */
16761
+ async _captureAndConverge(collectionName, docId, action, baseV) {
16762
+ const coll = this.collectionCache.get(collectionName);
16763
+ if (!coll) return null;
16764
+ const local = coll._peekCached(docId);
16765
+ let base = null;
16766
+ try {
16767
+ base = await coll.getVersion(docId, baseV);
16768
+ } catch {
16769
+ base = null;
16770
+ }
16771
+ await coll._applyRemoteChange(docId, action);
16772
+ const remote = await coll.get(docId);
16773
+ return { local, remote, base };
16774
+ }
16775
+ /** Recover a stuck cutover fence (#232) — reset to normal without bumping. */
16776
+ async abortSchemaCutover() {
16777
+ await this.schemaFence.abort();
16778
+ }
16779
+ /** Current schema-cutover fence state for this vault (#232/#233). Thin live read. */
16780
+ async schemaFenceState() {
16781
+ return loadFence(this.adapter, this.name);
16782
+ }
16783
+ /** @internal Start the per-client heartbeat + fence watcher once a cutover is registered (#232). */
16784
+ _ensureFenceCoordination() {
16785
+ if (this.#fenceCoordinationStarted) return;
16786
+ this.#fenceCoordinationStarted = true;
16787
+ this.#fenceWatcher = new FenceWatcher({
16788
+ store: this.adapter,
16789
+ vault: this.name,
16790
+ clientId: this.noydb._clientId,
16791
+ onFlush: () => this.noydb._writeQueueTracker.onFlush(),
16792
+ emit: (e) => this.emitter.emit("schema:fence-changed", { vault: this.name, ...e })
16793
+ });
16794
+ this.#fenceWatcher.start(2e3);
16795
+ }
16796
+ /** @internal Stop the heartbeat/watcher (vault lock/close). */
16797
+ _stopFenceCoordination() {
16798
+ this.#fenceWatcher?.stop();
16799
+ this.#fenceWatcher = void 0;
16800
+ this.#fenceCoordinationStarted = false;
16801
+ }
16802
+ /** @internal Drive one heartbeat + watch cycle deterministically (tests). */
16803
+ async _fenceTick() {
16804
+ this._ensureFenceCoordination();
16805
+ await this.#fenceWatcher.beat();
16806
+ await this.#fenceWatcher.check();
16807
+ }
16123
16808
  /**
16124
16809
  * Validate i18nText fields on a `put()`. Called by Collection just
16125
16810
  * before the adapter write, after schema validation. Throws
@@ -17548,6 +18233,27 @@ var Vault = class {
17548
18233
  async dumpSchema(opts = {}) {
17549
18234
  return dumpVaultSchema(this, opts);
17550
18235
  }
18236
+ /**
18237
+ * Lightweight read of the vault's registered schema (#229): collections
18238
+ * (+ doc counts), guards, materialized views, schema-update strategies,
18239
+ * and the unlocked user's grants. Cheap — one `adapter.list` per
18240
+ * collection, no decryption. For a full snapshot + stats use dumpSchema().
18241
+ * Post-unlock by construction (a Vault only exists with an unlocked keyring).
18242
+ */
18243
+ async introspect() {
18244
+ const byCol = (a, b) => a.collection.localeCompare(b.collection);
18245
+ const names = [.../* @__PURE__ */ new Set([...this.collectionCache.keys(), ...await this.collections()])].filter((n) => !n.startsWith("_")).sort((a, b) => a.localeCompare(b));
18246
+ const collections = [];
18247
+ for (const name of names) {
18248
+ const ids = await this.adapter.list(this.name, name);
18249
+ collections.push({ name, docCount: ids.length });
18250
+ }
18251
+ const guards = (this._getGuardRegistry()?.summary() ?? []).slice().sort(byCol);
18252
+ const materializedViews = (this._getMaterializedViewRegistry()?.all() ?? []).map((mv) => ({ name: mv.spec.name, sourceCollections: [...mv.dependencies].sort() })).sort((a, b) => a.name.localeCompare(b.name));
18253
+ const schemaUpdate = [...this.#schemaUpdateNames.entries()].map(([collection, strategies]) => ({ collection, strategies })).sort(byCol);
18254
+ const grants = [...this.keyring.deks.keys()].filter((collection) => !collection.startsWith("_")).map((collection) => ({ collection, permission: this.keyring.permissions[collection] ?? "rw" })).sort(byCol);
18255
+ return { collections, guards, materializedViews, schemaUpdate, grants };
18256
+ }
17551
18257
  /**
17552
18258
  * Internal accessor for {@link dumpVaultSchema}. Exposes the structural
17553
18259
  * state the walker needs (collection cache, registries, ref registry,
@@ -18187,6 +18893,387 @@ var NoydbEventEmitter = class {
18187
18893
  }
18188
18894
  };
18189
18895
 
18896
+ // src/write-queue.ts
18897
+ var WriteQueueTracker = class {
18898
+ #depth = 0;
18899
+ #error = null;
18900
+ #changeHandlers = /* @__PURE__ */ new Set();
18901
+ #flushWaiters = [];
18902
+ get pending() {
18903
+ return this.#depth > 0;
18904
+ }
18905
+ get depth() {
18906
+ return this.#depth;
18907
+ }
18908
+ /** Mark one write as started. */
18909
+ begin() {
18910
+ this.#depth++;
18911
+ this.#emitChange();
18912
+ }
18913
+ /** Mark one write as finished. Pass the error if it failed. */
18914
+ settle(error) {
18915
+ this.#depth = Math.max(0, this.#depth - 1);
18916
+ if (error) this.#error = error;
18917
+ this.#emitChange();
18918
+ if (this.#depth === 0) this.#drainFlush();
18919
+ }
18920
+ onChange(handler) {
18921
+ this.#changeHandlers.add(handler);
18922
+ return () => {
18923
+ this.#changeHandlers.delete(handler);
18924
+ };
18925
+ }
18926
+ onFlush() {
18927
+ if (this.#depth === 0) {
18928
+ const error = this.#error;
18929
+ this.#error = null;
18930
+ return error ? Promise.reject(error) : Promise.resolve();
18931
+ }
18932
+ return new Promise((resolve, reject) => {
18933
+ this.#flushWaiters.push({ resolve, reject });
18934
+ });
18935
+ }
18936
+ /**
18937
+ * Run `fn` as a tracked write: depth++ on entry, depth-- on settle
18938
+ * (success or failure). The fn's resolved value is returned; a thrown
18939
+ * error is re-thrown after the queue is decremented.
18940
+ */
18941
+ async track(fn) {
18942
+ this.begin();
18943
+ try {
18944
+ const value = await fn();
18945
+ this.settle();
18946
+ return value;
18947
+ } catch (error) {
18948
+ this.settle(error);
18949
+ throw error;
18950
+ }
18951
+ }
18952
+ #emitChange() {
18953
+ for (const handler of this.#changeHandlers) handler();
18954
+ }
18955
+ #drainFlush() {
18956
+ const waiters = this.#flushWaiters;
18957
+ this.#flushWaiters = [];
18958
+ const error = this.#error;
18959
+ this.#error = null;
18960
+ for (const waiter of waiters) {
18961
+ if (error) waiter.reject(error);
18962
+ else waiter.resolve();
18963
+ }
18964
+ }
18965
+ };
18966
+
18967
+ // src/write-hooks.ts
18968
+ var WriteHookRegistry = class {
18969
+ #before = [];
18970
+ #after = [];
18971
+ #suppressed = false;
18972
+ /** True while handlers are running — used by the write path to skip nested firing. */
18973
+ get suppressed() {
18974
+ return this.#suppressed;
18975
+ }
18976
+ /** True when any hook is registered (cheap gate for the write path). */
18977
+ get hasHandlers() {
18978
+ return this.#before.length > 0 || this.#after.length > 0;
18979
+ }
18980
+ onBeforeWrite(handler) {
18981
+ this.#before.push(handler);
18982
+ return () => {
18983
+ const i = this.#before.indexOf(handler);
18984
+ if (i >= 0) this.#before.splice(i, 1);
18985
+ };
18986
+ }
18987
+ onAfterWrite(handler) {
18988
+ this.#after.push(handler);
18989
+ return () => {
18990
+ const i = this.#after.indexOf(handler);
18991
+ if (i >= 0) this.#after.splice(i, 1);
18992
+ };
18993
+ }
18994
+ /** Run before-hooks (awaited, in order). A throw propagates and aborts the write. */
18995
+ async runBefore(event) {
18996
+ if (this.#before.length === 0) return;
18997
+ this.#suppressed = true;
18998
+ try {
18999
+ for (const h of this.#before.slice()) await h(event);
19000
+ } finally {
19001
+ this.#suppressed = false;
19002
+ }
19003
+ }
19004
+ /** Run after-hooks (awaited, in order). Per-handler errors are warned, not thrown. */
19005
+ async runAfter(event) {
19006
+ if (this.#after.length === 0) return;
19007
+ this.#suppressed = true;
19008
+ try {
19009
+ for (const h of this.#after.slice()) {
19010
+ try {
19011
+ await h(event);
19012
+ } catch (err) {
19013
+ console.warn(
19014
+ `[noy-db] onAfterWrite handler failed for ${event.collection}/${event.docId}: ` + (err instanceof Error ? err.message : String(err))
19015
+ );
19016
+ }
19017
+ }
19018
+ } finally {
19019
+ this.#suppressed = false;
19020
+ }
19021
+ }
19022
+ };
19023
+
19024
+ // src/tab-coordination.ts
19025
+ var TabCoordinator = class {
19026
+ tabId;
19027
+ role = "unknown";
19028
+ #lockManager;
19029
+ #channel;
19030
+ #lockName;
19031
+ #heartbeatMs;
19032
+ #staleMs;
19033
+ #now;
19034
+ #peers = /* @__PURE__ */ new Map();
19035
+ #roleHandlers = /* @__PURE__ */ new Set();
19036
+ #tabsHandlers = /* @__PURE__ */ new Set();
19037
+ #ac;
19038
+ #releaseLock;
19039
+ #unsub;
19040
+ #closeUnsub;
19041
+ #timer;
19042
+ #ownsChannel;
19043
+ #started = false;
19044
+ #disposed = false;
19045
+ #lastTabsSig = "";
19046
+ constructor(opts = {}) {
19047
+ this.tabId = opts.tabId ?? `tab-${Math.trunc((opts.now ?? (() => 0))())}-${cheapRand()}`;
19048
+ this.#lockManager = opts.lockManager;
19049
+ this.#channel = opts.channel;
19050
+ this.#lockName = opts.lockName ?? "noydb:tab-primary";
19051
+ this.#heartbeatMs = opts.heartbeatMs ?? 2e3;
19052
+ this.#staleMs = opts.staleMs ?? 6e3;
19053
+ this.#now = opts.now ?? (() => Date.now());
19054
+ this.#ownsChannel = opts.closeChannelOnDispose ?? false;
19055
+ }
19056
+ start() {
19057
+ if (this.#disposed || this.#started) return;
19058
+ this.#started = true;
19059
+ if (this.#channel) {
19060
+ this.#unsub = this.#channel.on("message", (p) => this.#onMessage(p));
19061
+ this.#closeUnsub = this.#channel.on("close", () => this.#onChannelClose());
19062
+ this.#beat();
19063
+ this.#timer = setInterval(() => this.#tick(), this.#heartbeatMs);
19064
+ const t = this.#timer;
19065
+ if (typeof t.unref === "function") t.unref();
19066
+ }
19067
+ if (this.#lockManager) {
19068
+ this.#ac = new AbortController();
19069
+ this.#setRole("secondary");
19070
+ void this.#lockManager.request(this.#lockName, { mode: "exclusive", signal: this.#ac.signal }, () => {
19071
+ this.#setRole("primary");
19072
+ return new Promise((resolve) => {
19073
+ this.#releaseLock = resolve;
19074
+ });
19075
+ }).catch(() => {
19076
+ });
19077
+ }
19078
+ }
19079
+ activeTabs() {
19080
+ if (!this.#channel) return [];
19081
+ const cutoff = this.#now() - this.#staleMs;
19082
+ const self = { tabId: this.tabId, lastSeen: this.#now(), role: this.role };
19083
+ const out = [self, ...[...this.#peers.values()].filter((p) => p.lastSeen >= cutoff)];
19084
+ return out.sort((a, b) => a.tabId.localeCompare(b.tabId));
19085
+ }
19086
+ onTabRoleChange(fn) {
19087
+ this.#roleHandlers.add(fn);
19088
+ return () => this.#roleHandlers.delete(fn);
19089
+ }
19090
+ onActiveTabsChange(fn) {
19091
+ this.#tabsHandlers.add(fn);
19092
+ return () => this.#tabsHandlers.delete(fn);
19093
+ }
19094
+ dispose() {
19095
+ if (this.#disposed) return;
19096
+ this.#disposed = true;
19097
+ this.#releaseLock?.();
19098
+ this.#ac?.abort();
19099
+ if (this.#timer) {
19100
+ clearInterval(this.#timer);
19101
+ this.#timer = void 0;
19102
+ }
19103
+ this.#unsub?.();
19104
+ this.#closeUnsub?.();
19105
+ if (this.#ownsChannel) this.#channel?.close();
19106
+ this.#setRole("unknown");
19107
+ }
19108
+ /** @internal test seam — broadcast one heartbeat now. */
19109
+ _beat() {
19110
+ this.#beat();
19111
+ }
19112
+ #tick() {
19113
+ this.#prune();
19114
+ this.#emitTabs();
19115
+ this.#beat();
19116
+ }
19117
+ #beat() {
19118
+ if (this.#disposed) return;
19119
+ if (!this.#channel || !this.#channel.isOpen) return;
19120
+ const msg = { kind: "tab-presence", tabId: this.tabId, lastSeen: this.#now(), role: this.role };
19121
+ this.#channel.send(JSON.stringify(msg));
19122
+ }
19123
+ #onChannelClose() {
19124
+ if (this.#timer) {
19125
+ clearInterval(this.#timer);
19126
+ this.#timer = void 0;
19127
+ }
19128
+ this.#setRole("unknown");
19129
+ }
19130
+ #onMessage(payload) {
19131
+ let msg;
19132
+ try {
19133
+ msg = JSON.parse(payload);
19134
+ } catch {
19135
+ return;
19136
+ }
19137
+ if (!isPresenceMsg(msg) || msg.tabId === this.tabId) return;
19138
+ this.#peers.set(msg.tabId, { tabId: msg.tabId, lastSeen: msg.lastSeen, role: msg.role });
19139
+ this.#prune();
19140
+ this.#emitTabs();
19141
+ }
19142
+ #prune() {
19143
+ const cutoff = this.#now() - this.#staleMs;
19144
+ for (const [id, p] of this.#peers) if (p.lastSeen < cutoff) this.#peers.delete(id);
19145
+ }
19146
+ #setRole(role) {
19147
+ if (this.role === role) return;
19148
+ this.role = role;
19149
+ for (const h of this.#roleHandlers) h(role);
19150
+ this.#beat();
19151
+ this.#emitTabs();
19152
+ }
19153
+ #emitTabs() {
19154
+ const tabs = this.activeTabs();
19155
+ const sig = tabs.map((t) => `${t.tabId}:${t.role}`).join("|");
19156
+ if (sig === this.#lastTabsSig) return;
19157
+ this.#lastTabsSig = sig;
19158
+ for (const h of this.#tabsHandlers) h(tabs);
19159
+ }
19160
+ };
19161
+ function isPresenceMsg(x) {
19162
+ if (x === null || typeof x !== "object") return false;
19163
+ const o = x;
19164
+ return o["kind"] === "tab-presence" && typeof o["tabId"] === "string" && typeof o["lastSeen"] === "number" && (o["role"] === "primary" || o["role"] === "secondary" || o["role"] === "unknown");
19165
+ }
19166
+ function cheapRand() {
19167
+ const g = globalThis;
19168
+ return g.crypto?.randomUUID ? g.crypto.randomUUID().slice(0, 8) : "anon";
19169
+ }
19170
+ function defaultLockManager() {
19171
+ const nav = globalThis.navigator;
19172
+ return nav?.locks;
19173
+ }
19174
+ function defaultChannel(name = "noydb:tabs") {
19175
+ if (typeof globalThis.window === "undefined") return void 0;
19176
+ const Bc = globalThis.BroadcastChannel;
19177
+ if (!Bc) return void 0;
19178
+ const bc = new Bc(name);
19179
+ const msgListeners = /* @__PURE__ */ new Set();
19180
+ bc.onmessage = (e) => {
19181
+ for (const l of msgListeners) l(String(e.data));
19182
+ };
19183
+ return {
19184
+ isOpen: true,
19185
+ send(payload) {
19186
+ bc.postMessage(payload);
19187
+ },
19188
+ on(event, listener) {
19189
+ if (event === "message") {
19190
+ const l = listener;
19191
+ msgListeners.add(l);
19192
+ return () => msgListeners.delete(l);
19193
+ }
19194
+ return () => {
19195
+ };
19196
+ },
19197
+ close() {
19198
+ msgListeners.clear();
19199
+ bc.close();
19200
+ }
19201
+ };
19202
+ }
19203
+
19204
+ // src/tab-write-relay.ts
19205
+ var CrossTabWriteRelay = class {
19206
+ #channel;
19207
+ #writerId;
19208
+ #subscribeAfterWrite;
19209
+ #applyRemoteWrite;
19210
+ #reportConflict;
19211
+ #ledger = /* @__PURE__ */ new Map();
19212
+ #ownsChannel;
19213
+ #unsubMsg;
19214
+ #unsubWrite;
19215
+ #started = false;
19216
+ #disposed = false;
19217
+ constructor(opts) {
19218
+ this.#channel = opts.channel;
19219
+ this.#writerId = opts.writerId;
19220
+ this.#subscribeAfterWrite = opts.subscribeAfterWrite;
19221
+ this.#applyRemoteWrite = opts.applyRemoteWrite;
19222
+ this.#reportConflict = opts.reportConflict;
19223
+ this.#ownsChannel = opts.closeChannelOnDispose ?? false;
19224
+ }
19225
+ start() {
19226
+ if (this.#started || this.#disposed) return;
19227
+ this.#started = true;
19228
+ this.#unsubMsg = this.#channel.on("message", (p) => this.#onMessage(p));
19229
+ this.#unsubWrite = this.#subscribeAfterWrite((e) => this.#onLocalWrite(e));
19230
+ }
19231
+ dispose() {
19232
+ if (this.#disposed) return;
19233
+ this.#disposed = true;
19234
+ this.#unsubWrite?.();
19235
+ this.#unsubMsg?.();
19236
+ if (this.#ownsChannel) this.#channel.close();
19237
+ }
19238
+ #onLocalWrite(e) {
19239
+ if (this.#disposed || !this.#channel.isOpen) return;
19240
+ this.#ledger.set(ledgerKey(e.vault, e.collection, e.docId), e.version);
19241
+ const action = e.op === "delete" ? "delete" : "put";
19242
+ const msg = { kind: "tab-write", writerId: this.#writerId, vault: e.vault, collection: e.collection, docId: e.docId, action, baseV: e.baseVersion, v: e.version };
19243
+ this.#channel.send(JSON.stringify(msg));
19244
+ }
19245
+ #onMessage(payload) {
19246
+ if (this.#disposed) return;
19247
+ let msg;
19248
+ try {
19249
+ msg = JSON.parse(payload);
19250
+ } catch {
19251
+ return;
19252
+ }
19253
+ if (!isTabWriteMsg(msg) || msg.writerId === this.#writerId) return;
19254
+ const key = ledgerKey(msg.vault, msg.collection, msg.docId);
19255
+ const ownV = this.#ledger.get(key);
19256
+ if (ownV !== void 0 && msg.baseV < ownV && this.#reportConflict) {
19257
+ void Promise.resolve(this.#reportConflict(msg.vault, msg.collection, msg.docId, msg.action, msg.baseV, msg.v, ownV)).catch((err) => {
19258
+ console.warn(`[noy-db] cross-tab conflict report failed for ${msg.collection}/${msg.docId}: ` + (err instanceof Error ? err.message : String(err)));
19259
+ });
19260
+ return;
19261
+ }
19262
+ if (ownV !== void 0 && msg.baseV >= ownV) this.#ledger.set(key, msg.v);
19263
+ void Promise.resolve(this.#applyRemoteWrite(msg.vault, msg.collection, msg.docId, msg.action)).catch((err) => {
19264
+ console.warn(`[noy-db] cross-tab apply failed for ${msg.collection}/${msg.docId}: ` + (err instanceof Error ? err.message : String(err)));
19265
+ });
19266
+ }
19267
+ };
19268
+ function ledgerKey(vault, collection, docId) {
19269
+ return `${vault}\0${collection}\0${docId}`;
19270
+ }
19271
+ function isTabWriteMsg(x) {
19272
+ if (x === null || typeof x !== "object") return false;
19273
+ const o = x;
19274
+ 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";
19275
+ }
19276
+
18190
19277
  // src/tx/strategy.ts
18191
19278
  var NOT_ENABLED5 = new Error(
18192
19279
  'Multi-record transactions require the tx strategy. Import `{ withTransactions }` from "@noy-db/hub/tx" and pass it to `createNoydb({ txStrategy: withTransactions() })`.'
@@ -18194,6 +19281,9 @@ var NOT_ENABLED5 = new Error(
18194
19281
  var NO_TX = {
18195
19282
  async runTransaction() {
18196
19283
  throw NOT_ENABLED5;
19284
+ },
19285
+ async runDryRun() {
19286
+ throw NOT_ENABLED5;
18197
19287
  }
18198
19288
  };
18199
19289
 
@@ -18491,6 +19581,9 @@ function createPlaintextKeyring(userId) {
18491
19581
  var Noydb = class {
18492
19582
  options;
18493
19583
  emitter = new NoydbEventEmitter();
19584
+ writeQueueTracker = new WriteQueueTracker();
19585
+ writeHooks = new WriteHookRegistry();
19586
+ clientId = generateULID();
18494
19587
  vaultCache = /* @__PURE__ */ new Map();
18495
19588
  keyringCache = /* @__PURE__ */ new Map();
18496
19589
  syncEngines = /* @__PURE__ */ new Map();
@@ -18523,6 +19616,10 @@ var Noydb = class {
18523
19616
  publicEnvelopeSchema;
18524
19617
  closed = false;
18525
19618
  sessionTimer = null;
19619
+ /** Same-device multi-tab coordinator (#228); created on `enableTabCoordination()`. */
19620
+ tabCoordinator;
19621
+ /** Cross-tab write relay (#228b); created on `enableTabCoordination()`. */
19622
+ writeRelay;
18526
19623
  /** Per-vault policy enforcers. */
18527
19624
  policyEnforcers = /* @__PURE__ */ new Map();
18528
19625
  txStrategy;
@@ -18715,6 +19812,7 @@ var Noydb = class {
18715
19812
  await comp._initDerivations(this.options.derivationStrategies ?? []);
18716
19813
  await comp._initMaterializedViews(this.options.materializedViewStrategies ?? []);
18717
19814
  await comp._initOverlayedViews(this.options.overlayedViewStrategies ?? []);
19815
+ await comp.schemaFence.init();
18718
19816
  this.vaultCache.set(name, comp);
18719
19817
  return comp;
18720
19818
  }
@@ -19142,6 +20240,14 @@ var Noydb = class {
19142
20240
  if (typeof arg === "function") {
19143
20241
  return this.txStrategy.runTransaction(this, arg);
19144
20242
  }
20243
+ if (typeof arg === "object" && arg !== null && arg.dryRun === true) {
20244
+ if (typeof maybeFn !== "function") {
20245
+ throw new ValidationError(
20246
+ "db.transaction({ dryRun: true }, fn) requires the callback as the second argument."
20247
+ );
20248
+ }
20249
+ return this.txStrategy.runDryRun(this, maybeFn);
20250
+ }
19145
20251
  if (typeof arg === "object" && arg !== null && arg.amendment === true) {
19146
20252
  if (typeof maybeFn !== "function") {
19147
20253
  throw new ValidationError(
@@ -19254,6 +20360,133 @@ var Noydb = class {
19254
20360
  off(event, handler) {
19255
20361
  this.emitter.off(event, handler);
19256
20362
  }
20363
+ /**
20364
+ * Observable write-queue for this hub instance. Reflects outstanding
20365
+ * in-flight writes across all collections. See {@link WriteQueue}.
20366
+ *
20367
+ * @example
20368
+ * window.addEventListener('beforeunload', (e) => {
20369
+ * if (db.writeQueue.pending) { e.preventDefault(); e.returnValue = '' }
20370
+ * })
20371
+ */
20372
+ get writeQueue() {
20373
+ return this.writeQueueTracker;
20374
+ }
20375
+ /**
20376
+ * @internal Mutable tracker behind {@link writeQueue}. Threaded into
20377
+ * each Collection (via Vault) so `put`/`delete` can `track()` writes.
20378
+ * Not part of the public surface — consumers use `writeQueue`.
20379
+ */
20380
+ get _writeQueueTracker() {
20381
+ return this.writeQueueTracker;
20382
+ }
20383
+ /**
20384
+ * Register a hook that runs before each write (#230). Awaited; a throw
20385
+ * aborts the write. Returns an unsubscribe function.
20386
+ */
20387
+ onBeforeWrite(handler) {
20388
+ return this.writeHooks.onBeforeWrite(handler);
20389
+ }
20390
+ /**
20391
+ * Register a hook that runs after each committed write (#230). Awaited;
20392
+ * a handler error is warned, never rolled back. Returns an unsubscribe fn.
20393
+ */
20394
+ onAfterWrite(handler) {
20395
+ return this.writeHooks.onAfterWrite(handler);
20396
+ }
20397
+ /** Subscribe to cross-tab write conflicts (#228c). Returns an unsubscribe. */
20398
+ onWriteConflict(fn) {
20399
+ this.on("write:conflict", fn);
20400
+ return () => this.off("write:conflict", fn);
20401
+ }
20402
+ /**
20403
+ * Enable same-device multi-tab coordination (#228): primary/secondary
20404
+ * election + presence. Browser-only — a graceful no-op (role 'unknown')
20405
+ * when Web Locks / BroadcastChannel are unavailable and nothing is
20406
+ * injected. Idempotent; returns a disposer.
20407
+ */
20408
+ enableTabCoordination(opts = {}) {
20409
+ if (this.tabCoordinator) return { dispose: () => this.disableTabCoordination() };
20410
+ const lockManager = opts.lockManager ?? defaultLockManager();
20411
+ const channel = opts.channel ?? defaultChannel();
20412
+ const c = new TabCoordinator({
20413
+ ...opts,
20414
+ ...lockManager ? { lockManager } : {},
20415
+ ...channel ? { channel } : {},
20416
+ // We own the channel only when we created the default; never close a caller-injected one.
20417
+ closeChannelOnDispose: opts.channel === void 0 && channel !== void 0
20418
+ });
20419
+ this.tabCoordinator = c;
20420
+ c.start();
20421
+ if (opts.propagateWrites !== false) {
20422
+ const writeChannel = opts.writeChannel ?? defaultChannel("noydb:tab-writes");
20423
+ if (writeChannel) {
20424
+ const relay = new CrossTabWriteRelay({
20425
+ channel: writeChannel,
20426
+ writerId: c.tabId,
20427
+ subscribeAfterWrite: (h) => this.onAfterWrite(h),
20428
+ applyRemoteWrite: (vault, collection, docId, action) => this.#applyRemoteWrite(vault, collection, docId, action),
20429
+ reportConflict: (vault, collection, docId, action, baseV, v, ownV) => this.#reportWriteConflict(vault, collection, docId, action, baseV, v, ownV),
20430
+ // Own the channel only when we created the default (mirrors the presence channel).
20431
+ closeChannelOnDispose: opts.writeChannel === void 0 && writeChannel !== void 0
20432
+ });
20433
+ this.writeRelay = relay;
20434
+ relay.start();
20435
+ }
20436
+ }
20437
+ return { dispose: () => this.disableTabCoordination() };
20438
+ }
20439
+ #applyRemoteWrite(vaultName, collectionName, docId, action) {
20440
+ const v = this.vaultCache.get(vaultName);
20441
+ if (!v) return Promise.resolve();
20442
+ return v._applyRemoteWrite(collectionName, docId, action);
20443
+ }
20444
+ async #reportWriteConflict(vaultName, collectionName, docId, action, baseV, v, ownV) {
20445
+ const vault = this.vaultCache.get(vaultName);
20446
+ if (!vault) return;
20447
+ const cap = await vault._captureAndConverge(collectionName, docId, action, baseV);
20448
+ if (!cap) return;
20449
+ const conflict = {
20450
+ vault: vaultName,
20451
+ collection: collectionName,
20452
+ docId,
20453
+ local: cap.local,
20454
+ remote: cap.remote,
20455
+ base: cap.base,
20456
+ localVersion: ownV,
20457
+ remoteVersion: v,
20458
+ baseVersion: baseV
20459
+ };
20460
+ this.emitter.emit("write:conflict", conflict);
20461
+ }
20462
+ disableTabCoordination() {
20463
+ this.tabCoordinator?.dispose();
20464
+ this.tabCoordinator = void 0;
20465
+ this.writeRelay?.dispose();
20466
+ this.writeRelay = void 0;
20467
+ }
20468
+ get tabRole() {
20469
+ return this.tabCoordinator?.role ?? "unknown";
20470
+ }
20471
+ activeTabs() {
20472
+ return this.tabCoordinator?.activeTabs() ?? [];
20473
+ }
20474
+ onTabRoleChange(fn) {
20475
+ return this.tabCoordinator?.onTabRoleChange(fn) ?? (() => {
20476
+ });
20477
+ }
20478
+ onActiveTabsChange(fn) {
20479
+ return this.tabCoordinator?.onActiveTabsChange(fn) ?? (() => {
20480
+ });
20481
+ }
20482
+ /** @internal The write-hook registry, threaded into each Collection. */
20483
+ get _writeHooks() {
20484
+ return this.writeHooks;
20485
+ }
20486
+ /** @internal Stable per-instance id for schema-cutover coordination (#232). */
20487
+ get _clientId() {
20488
+ return this.clientId;
20489
+ }
19257
20490
  /**
19258
20491
  * Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
19259
20492
  * instance, sync engine, policy enforcer, and active-tier entry —
@@ -19280,6 +20513,7 @@ var Noydb = class {
19280
20513
  this.syncEngines.delete(vault);
19281
20514
  this.policyEnforcers.get(vault)?.destroy();
19282
20515
  this.policyEnforcers.delete(vault);
20516
+ this.vaultCache.get(vault)?._stopFenceCoordination();
19283
20517
  this.keyringCache.delete(vault);
19284
20518
  this.vaultCache.delete(vault);
19285
20519
  this.activeTier.delete(vault);
@@ -19299,6 +20533,8 @@ var Noydb = class {
19299
20533
  engine.stopAutoSync();
19300
20534
  }
19301
20535
  this.syncEngines.clear();
20536
+ for (const v of this.vaultCache.values()) v._stopFenceCoordination();
20537
+ this.disableTabCoordination();
19302
20538
  this.keyringCache.clear();
19303
20539
  this.vaultCache.clear();
19304
20540
  this.activeTier.clear();
@@ -22101,6 +23337,7 @@ function shortJSON(value) {
22101
23337
  MaterializedViewTooLargeError,
22102
23338
  MemoryRecipientSealer,
22103
23339
  MemorySealingKeyProvider,
23340
+ MigrationRequiredError,
22104
23341
  MissingTranslationError,
22105
23342
  NOYDB_BACKUP_VERSION,
22106
23343
  NOYDB_BUNDLE_FORMAT_VERSION,
@@ -22111,6 +23348,7 @@ function shortJSON(value) {
22111
23348
  NOYDB_SYNC_VERSION,
22112
23349
  NetworkError,
22113
23350
  NoAccessError,
23351
+ NonAdditiveSchemaChangeError,
22114
23352
  NotFoundError,
22115
23353
  Noydb,
22116
23354
  NoydbError,
@@ -22132,6 +23370,7 @@ function shortJSON(value) {
22132
23370
  PrivilegeEscalationError,
22133
23371
  Query,
22134
23372
  QuickUnlockStore,
23373
+ QuiesceTimeoutError,
22135
23374
  ReadOnlyAtInstantError,
22136
23375
  ReadOnlyError,
22137
23376
  ReadOnlyFrameError,
@@ -22147,6 +23386,9 @@ function shortJSON(value) {
22147
23386
  STRICT_POLICY,
22148
23387
  SYNC_CREDENTIALS_COLLECTION,
22149
23388
  ScanBuilder,
23389
+ SchemaFenceError,
23390
+ SchemaLockedError,
23391
+ SchemaUpdateError,
22150
23392
  SchemaValidationError,
22151
23393
  SessionExpiredError,
22152
23394
  SessionNotFoundError,
@@ -22173,6 +23415,7 @@ function shortJSON(value) {
22173
23415
  VaultInstant,
22174
23416
  WeakPassphraseError,
22175
23417
  activeSessionCount,
23418
+ additiveOnly,
22176
23419
  applyI18nLocale,
22177
23420
  applyJoins,
22178
23421
  applyPatch,
@@ -22180,6 +23423,7 @@ function shortJSON(value) {
22180
23423
  assertTierAccess,
22181
23424
  avg,
22182
23425
  base64ToBuffer,
23426
+ blindUpdate,
22183
23427
  bufferToBase64,
22184
23428
  buildLiveQuery,
22185
23429
  buildRecipientKeyringFile,
@@ -22188,6 +23432,7 @@ function shortJSON(value) {
22188
23432
  checkGate,
22189
23433
  clearDevUnlock,
22190
23434
  computePatch,
23435
+ coordinatedCutover,
22191
23436
  count,
22192
23437
  createBundleStore,
22193
23438
  createEnforcer,
@@ -22266,6 +23511,7 @@ function shortJSON(value) {
22266
23511
  loadShamirRecoveryEntries,
22267
23512
  loadUserEnvelope,
22268
23513
  loadVaultPolicy,
23514
+ lockSchema,
22269
23515
  magicLinkGrantRecordId,
22270
23516
  max,
22271
23517
  mergeCrdtStates,