@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
@@ -4,34 +4,34 @@ import {
4
4
  import {
5
5
  TxContext,
6
6
  revertExecuted
7
- } from "./chunk-3Z2TPHC4.js";
7
+ } from "./chunk-GVXBHCZ2.js";
8
8
  import {
9
9
  OverlayedCollection
10
- } from "./chunk-YTXSFG3C.js";
10
+ } from "./chunk-NGSPBLLE.js";
11
11
  import {
12
12
  LazyQuery,
13
13
  decodeIdxId,
14
14
  encodeIdxId
15
- } from "./chunk-7BRE6EUA.js";
15
+ } from "./chunk-WRLHNG6H.js";
16
16
  import {
17
17
  SCHEMAS_COLLECTION,
18
18
  loadPersistedSchema,
19
19
  resolveManagedSecret,
20
20
  savePersistedSchema,
21
21
  saveSealedPassphrase
22
- } from "./chunk-UND4XIB6.js";
22
+ } from "./chunk-4X2S7PBF.js";
23
23
  import {
24
24
  loadPublicEnvelope,
25
25
  readPublicEnvelope,
26
26
  savePublicEnvelope,
27
27
  validatePublicEnvelopeInput
28
- } from "./chunk-243PNUA6.js";
28
+ } from "./chunk-FS7A4XNF.js";
29
29
  import {
30
30
  PERIODS_COLLECTION
31
- } from "./chunk-QPEXPHJR.js";
31
+ } from "./chunk-GAUBWHAF.js";
32
32
  import {
33
33
  isDictCollectionName
34
- } from "./chunk-LRAZDV5X.js";
34
+ } from "./chunk-KMI2NBBF.js";
35
35
  import {
36
36
  ManagedRecoveryNotEnrolledError,
37
37
  PolicyDeniedError,
@@ -53,11 +53,11 @@ import {
53
53
  saveShamirRecoveryEntries,
54
54
  updateAuthenticator,
55
55
  writeMagicLinkGrant
56
- } from "./chunk-EUYOGYGV.js";
56
+ } from "./chunk-2EYC3WDT.js";
57
57
  import {
58
58
  assertTierAccess,
59
59
  dekKey
60
- } from "./chunk-7BUTTVMR.js";
60
+ } from "./chunk-6S3LLAQ5.js";
61
61
  import {
62
62
  USER_ENVELOPE_COLLECTION,
63
63
  buildRecipientKeyringFile,
@@ -81,7 +81,7 @@ import {
81
81
  rotateKeys,
82
82
  saveUserEnvelope,
83
83
  updateKeyringIdentity
84
- } from "./chunk-Q6W2CMEJ.js";
84
+ } from "./chunk-TLFUDXVV.js";
85
85
  import {
86
86
  INDEXED_STORE_POLICY
87
87
  } from "./chunk-2QR2PQTT.js";
@@ -91,30 +91,30 @@ import {
91
91
  import {
92
92
  LEDGER_COLLECTION,
93
93
  LEDGER_DELTAS_COLLECTION
94
- } from "./chunk-7Z23ZFLV.js";
94
+ } from "./chunk-74JEQFMT.js";
95
95
  import {
96
96
  sha256Hex as sha256Hex2
97
- } from "./chunk-XG3PTSCD.js";
97
+ } from "./chunk-NCO2JGKK.js";
98
98
  import {
99
99
  NO_AGGREGATE,
100
100
  Query,
101
101
  ScanBuilder
102
- } from "./chunk-J4KLMEUL.js";
102
+ } from "./chunk-4UBOTYP5.js";
103
103
  import {
104
104
  EXPORT_AUDIT_COLLECTION,
105
105
  createExportBlobsHandle,
106
106
  runCompaction
107
- } from "./chunk-PFSNOPBQ.js";
107
+ } from "./chunk-LOL725S4.js";
108
108
  import {
109
109
  NOYDB_BACKUP_VERSION,
110
110
  NOYDB_FORMAT_VERSION
111
- } from "./chunk-YS3POABP.js";
111
+ } from "./chunk-FXQYZNOW.js";
112
112
  import {
113
113
  decrypt,
114
114
  encrypt,
115
115
  encryptDeterministic,
116
116
  sha256Hex
117
- } from "./chunk-2PAQNPE3.js";
117
+ } from "./chunk-UOF74WQY.js";
118
118
  import {
119
119
  AlreadyElevatedError,
120
120
  AttestationError,
@@ -127,18 +127,21 @@ import {
127
127
  IndexWriteFailureError,
128
128
  InvalidKeyError,
129
129
  KeyringCorruptError,
130
+ MigrationRequiredError,
130
131
  NoAccessError,
131
132
  NoydbError,
132
133
  PermissionDeniedError,
134
+ QuiesceTimeoutError,
133
135
  ReadOnlyError,
134
136
  ReservedCollectionNameError,
137
+ SchemaFenceError,
135
138
  SchemaValidationError,
136
139
  StoreCapabilityError,
137
140
  TierDemoteDeniedError,
138
141
  TierNotGrantedError,
139
142
  TranslatorNotConfiguredError,
140
143
  ValidationError
141
- } from "./chunk-W3XXT26A.js";
144
+ } from "./chunk-YDLAFP36.js";
142
145
 
143
146
  // src/policy/storage.ts
144
147
  var META_COLLECTION = "_meta";
@@ -641,7 +644,7 @@ async function resolveStaleOnRead(accessor, outputCollection, id) {
641
644
  }
642
645
  const sourceWithId = { ...source, id };
643
646
  if (DerivationExecutor === null) {
644
- ({ DerivationExecutor } = await import("./executor-HLXFXNFM.js"));
647
+ ({ DerivationExecutor } = await import("./executor-GFZFDQXV.js"));
645
648
  }
646
649
  const ctx = { vault: accessor.getReadOnlyFacade() };
647
650
  const result = await DerivationExecutor.run(spec, sourceWithId, 0, strategyHash, ctx);
@@ -695,6 +698,11 @@ var Collection = class {
695
698
  keyring;
696
699
  encrypted;
697
700
  emitter;
701
+ writeQueue;
702
+ schemaUpdateGate;
703
+ schemaFence;
704
+ writeHooks;
705
+ activeTxId;
698
706
  getDEK;
699
707
  onDirty;
700
708
  historyConfig;
@@ -969,6 +977,11 @@ var Collection = class {
969
977
  this.keyring = opts.keyring;
970
978
  this.encrypted = opts.encrypted;
971
979
  this.emitter = opts.emitter;
980
+ this.writeQueue = opts.writeQueue;
981
+ this.schemaUpdateGate = opts.schemaUpdateGate;
982
+ this.schemaFence = opts.schemaFence;
983
+ this.writeHooks = opts.writeHooks;
984
+ this.activeTxId = opts.activeTxId;
972
985
  this.blobStrategy = opts.blobStrategy ?? NO_BLOBS;
973
986
  this.aggregateStrategy = opts.aggregateStrategy ?? NO_AGGREGATE;
974
987
  this.crdtStrategy = opts.crdtStrategy ?? NO_CRDT;
@@ -1127,7 +1140,7 @@ var Collection = class {
1127
1140
  }
1128
1141
  }
1129
1142
  if (this.materializedViewSource !== void 0) {
1130
- const { resolveStaleMVOnRead } = await import("./stale-OTOF3FH7.js");
1143
+ const { resolveStaleMVOnRead } = await import("./stale-PAGCS4K5.js");
1131
1144
  await resolveStaleMVOnRead(this.materializedViewSource, this.name);
1132
1145
  }
1133
1146
  let record;
@@ -1194,7 +1207,8 @@ var Collection = class {
1194
1207
  return this.syncStrategy.buildPresence(presenceOpts);
1195
1208
  }
1196
1209
  /**
1197
- * Create or update a record.
1210
+ * Create or update a record. Runs inside the hub's write-queue tracker
1211
+ * (#227) so `hub.writeQueue.pending` reflects this write.
1198
1212
  *
1199
1213
  * @param id Record identifier.
1200
1214
  * @param record The record body (validated by the collection's schema
@@ -1205,6 +1219,59 @@ var Collection = class {
1205
1219
  * `entries.filter(e => e.reason?.startsWith('import:'))`.
1206
1220
  */
1207
1221
  async put(id, record, options) {
1222
+ await this.schemaUpdateGate?.assertWritable();
1223
+ await this.schemaFence?.assertWritable(this.name);
1224
+ let event;
1225
+ if (this.#hooksActive()) {
1226
+ const prior = await this.#priorForHook(id);
1227
+ event = {
1228
+ op: prior.record === null ? "create" : "update",
1229
+ vault: this.vault,
1230
+ collection: this.name,
1231
+ docId: id,
1232
+ before: prior.record,
1233
+ after: record,
1234
+ userId: this.keyring.userId,
1235
+ timestamp: Date.now(),
1236
+ txId: this.#txIdForHook(),
1237
+ baseVersion: prior.version,
1238
+ version: prior.version + 1
1239
+ };
1240
+ await this.writeHooks.runBefore(event);
1241
+ }
1242
+ if (this.writeQueue) await this.writeQueue.track(() => this.putInternal(id, record, options));
1243
+ else await this.putInternal(id, record, options);
1244
+ if (event) await this.writeHooks.runAfter(event);
1245
+ }
1246
+ /** @internal #230 — true when hooks should fire for this write (handlers exist, not re-entrant). */
1247
+ #hooksActive() {
1248
+ return this.writeHooks !== void 0 && this.writeHooks.hasHandlers && !this.writeHooks.suppressed;
1249
+ }
1250
+ /**
1251
+ * @internal #230/#228c — resolve the prior record for a hook's `before` and
1252
+ * its version. Critically, this uses the SAME basis `putInternal` writes from
1253
+ * (the in-memory cache in eager mode; lru-then-adapter in lazy) — NOT a fresh
1254
+ * store read — so `baseVersion`/`version` match the version actually written.
1255
+ * A separate store read would diverge once another tab has advanced the shared
1256
+ * store past this tab's cache, breaking #228c conflict detection.
1257
+ */
1258
+ async #priorForHook(id) {
1259
+ if (this.lazy && this.lru) {
1260
+ const cached2 = this.lru.get(id);
1261
+ if (cached2) return { record: cached2.record, version: cached2.version };
1262
+ const env = await this.adapter.get(this.vault, this.name, id);
1263
+ if (!env) return { record: null, version: 0 };
1264
+ return { record: await this.decryptRecord(env, { skipValidation: true }), version: env._v };
1265
+ }
1266
+ await this.ensureHydrated();
1267
+ const cached = this.cache.get(id);
1268
+ return cached ? { record: cached.record, version: cached.version } : { record: null, version: 0 };
1269
+ }
1270
+ #txIdForHook() {
1271
+ return this.activeTxId?.() ?? generateULID();
1272
+ }
1273
+ /** @internal Untracked put body — call {@link put}, not this. */
1274
+ async putInternal(id, record, options) {
1208
1275
  if (!hasWritePermission(this.keyring, this.name)) {
1209
1276
  throw new ReadOnlyError();
1210
1277
  }
@@ -1233,7 +1300,7 @@ var Collection = class {
1233
1300
  registry.collectChange(this.name, id, existingRecord, incomingRecord, vBefore, vBefore + 1);
1234
1301
  } else {
1235
1302
  await registry.runChecks(this.name, incomingRecord, ctx);
1236
- const { GuardExecutor } = await import("./executor-HN6YBHZ5.js");
1303
+ const { GuardExecutor } = await import("./executor-BZKFZVRC.js");
1237
1304
  for (const g of guards) {
1238
1305
  await GuardExecutor.checkFrozenFields(g, id, existingRecord, incomingRecord);
1239
1306
  }
@@ -1462,7 +1529,7 @@ var Collection = class {
1462
1529
  if (mode === "eager") {
1463
1530
  if (executor === null) {
1464
1531
  ;
1465
- ({ MaterializedViewExecutor: executor } = await import("./executor-AS2IDHKZ.js"));
1532
+ ({ MaterializedViewExecutor: executor } = await import("./executor-KT2IOZVP.js"));
1466
1533
  }
1467
1534
  await executor.refresh(reg, {
1468
1535
  getCollection: (name) => this.materializedViewSource.getCollection(name),
@@ -1471,7 +1538,7 @@ var Collection = class {
1471
1538
  });
1472
1539
  } else if (mode === "lazy") {
1473
1540
  if (staleHelpers === null) {
1474
- staleHelpers = await import("./stale-OTOF3FH7.js");
1541
+ staleHelpers = await import("./stale-PAGCS4K5.js");
1475
1542
  }
1476
1543
  staleHelpers.markMVStale(registry, reg.spec.name);
1477
1544
  }
@@ -1500,7 +1567,7 @@ var Collection = class {
1500
1567
  const mode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
1501
1568
  if (mode === "eager") {
1502
1569
  if (DerivationExecutor === null) {
1503
- ({ DerivationExecutor } = await import("./executor-HLXFXNFM.js"));
1570
+ ({ DerivationExecutor } = await import("./executor-GFZFDQXV.js"));
1504
1571
  }
1505
1572
  const sourceWithId = { ...incoming, id };
1506
1573
  const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
@@ -1519,7 +1586,7 @@ var Collection = class {
1519
1586
  const outputCollection = this.derivationSource.getCollection(outSpec.collection);
1520
1587
  const txCtx = this.derivationSource.getActiveTxContext();
1521
1588
  if (out.kind === "array") {
1522
- const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-VJ52RIEY.js");
1589
+ const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-NRBWSLRK.js");
1523
1590
  const prior = await loadFanoutSidecar(
1524
1591
  this.adapter,
1525
1592
  this.vault,
@@ -1581,8 +1648,71 @@ var Collection = class {
1581
1648
  }
1582
1649
  }
1583
1650
  }
1584
- /** Delete a record by ID. */
1651
+ /**
1652
+ * Delete a record by ID. Runs inside the hub's write-queue tracker
1653
+ * (#227) so `hub.writeQueue.pending` reflects this write.
1654
+ */
1585
1655
  async delete(id) {
1656
+ await this.schemaUpdateGate?.assertWritable();
1657
+ await this.schemaFence?.assertWritable(this.name);
1658
+ let event;
1659
+ if (this.#hooksActive()) {
1660
+ const prior = await this.#priorForHook(id);
1661
+ event = {
1662
+ op: "delete",
1663
+ vault: this.vault,
1664
+ collection: this.name,
1665
+ docId: id,
1666
+ before: prior.record,
1667
+ after: null,
1668
+ userId: this.keyring.userId,
1669
+ timestamp: Date.now(),
1670
+ txId: this.#txIdForHook(),
1671
+ baseVersion: prior.version,
1672
+ version: prior.version + 1
1673
+ };
1674
+ await this.writeHooks.runBefore(event);
1675
+ }
1676
+ if (this.writeQueue) await this.writeQueue.track(() => this.deleteInternal(id));
1677
+ else await this.deleteInternal(id);
1678
+ if (event) await this.writeHooks.runAfter(event);
1679
+ }
1680
+ /**
1681
+ * @internal #232 — bulk-rewrite every record through a cutover transform.
1682
+ * Raw adapter path (bypasses the write gate + guards — the transform is
1683
+ * trusted and runs only during the `migrating` phase). Bumps each
1684
+ * record's `_v` and appends a ledger `op:'migration'` entry.
1685
+ */
1686
+ async _applyCutoverTransform(transform) {
1687
+ const ids = await this.adapter.list(this.vault, this.name);
1688
+ let count = 0;
1689
+ for (const id of ids) {
1690
+ const env = await this.adapter.get(this.vault, this.name, id);
1691
+ if (!env) continue;
1692
+ const record = await this.decryptRecord(env, { skipValidation: true });
1693
+ const next = transform(record);
1694
+ const nextVersion = (env._v ?? 0) + 1;
1695
+ const newEnv = await this.encryptRecord(next, nextVersion);
1696
+ await this.adapter.put(this.vault, this.name, id, newEnv);
1697
+ await this._invalidateCacheEntry(id);
1698
+ if (this.ledger) {
1699
+ await this.ledger.append({
1700
+ op: "migration",
1701
+ collection: this.name,
1702
+ id,
1703
+ version: nextVersion,
1704
+ actor: this.keyring.userId,
1705
+ payloadHash: "",
1706
+ reason: "schema:coordinated-cutover"
1707
+ }).catch(() => {
1708
+ });
1709
+ }
1710
+ count++;
1711
+ }
1712
+ return count;
1713
+ }
1714
+ /** @internal Untracked delete body — call {@link delete}, not this. */
1715
+ async deleteInternal(id) {
1586
1716
  await this._doDelete(id, false);
1587
1717
  }
1588
1718
  /**
@@ -1775,7 +1905,7 @@ var Collection = class {
1775
1905
  for (const [outputKey, outSpec] of Object.entries(spec.outputs)) {
1776
1906
  if (outSpec.shape !== "array") continue;
1777
1907
  if (helpers === null) {
1778
- helpers = await import("./fanout-sidecar-VJ52RIEY.js");
1908
+ helpers = await import("./fanout-sidecar-NRBWSLRK.js");
1779
1909
  }
1780
1910
  const sidecar = await helpers.loadFanoutSidecar(
1781
1911
  this.adapter,
@@ -1815,7 +1945,7 @@ var Collection = class {
1815
1945
  if (mode === "eager") {
1816
1946
  if (executor === null) {
1817
1947
  ;
1818
- ({ MaterializedViewExecutor: executor } = await import("./executor-AS2IDHKZ.js"));
1948
+ ({ MaterializedViewExecutor: executor } = await import("./executor-KT2IOZVP.js"));
1819
1949
  }
1820
1950
  await executor.refresh(reg, {
1821
1951
  getCollection: (name) => this.materializedViewSource.getCollection(name),
@@ -1824,7 +1954,7 @@ var Collection = class {
1824
1954
  });
1825
1955
  } else if (mode === "lazy") {
1826
1956
  if (staleHelpers === null) {
1827
- staleHelpers = await import("./stale-OTOF3FH7.js");
1957
+ staleHelpers = await import("./stale-PAGCS4K5.js");
1828
1958
  }
1829
1959
  staleHelpers.markMVStale(registry, reg.spec.name);
1830
1960
  }
@@ -1847,7 +1977,7 @@ var Collection = class {
1847
1977
  );
1848
1978
  }
1849
1979
  if (this.materializedViewSource !== void 0) {
1850
- const { resolveStaleMVOnRead } = await import("./stale-OTOF3FH7.js");
1980
+ const { resolveStaleMVOnRead } = await import("./stale-PAGCS4K5.js");
1851
1981
  await resolveStaleMVOnRead(this.materializedViewSource, this.name);
1852
1982
  }
1853
1983
  await this.ensureHydrated();
@@ -2405,6 +2535,21 @@ var Collection = class {
2405
2535
  this.cache.set(id, { record, version: envelope._v });
2406
2536
  this.indexes?.upsert(id, record, previous ? previous.record : null);
2407
2537
  }
2538
+ /**
2539
+ * #228b — apply a peer tab's committed write to THIS tab's in-memory view:
2540
+ * re-read the (already-persisted) envelope from the shared store + refresh
2541
+ * cache/indexes, then emit a `change` event so reactive consumers re-render.
2542
+ * Never writes to the store and never fires write hooks, so it cannot loop.
2543
+ */
2544
+ async _applyRemoteChange(id, action) {
2545
+ await this._invalidateCacheEntry(id);
2546
+ this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action });
2547
+ }
2548
+ /** @internal #228c — the current in-memory record without a store read (for conflict capture). */
2549
+ _peekCached(id) {
2550
+ const entry = this.lazy && this.lru ? this.lru.get(id) : this.cache.get(id);
2551
+ return entry ? entry.record : null;
2552
+ }
2408
2553
  async ensureHydrated() {
2409
2554
  if (this.hydrated) return;
2410
2555
  const ids = await this.adapter.list(this.vault, this.name);
@@ -3802,15 +3947,68 @@ async function derivePersistedSchema(validator) {
3802
3947
  };
3803
3948
  }
3804
3949
 
3950
+ // src/schema-update/delta.ts
3951
+ function computeSchemaDelta(stored, fresh, collection) {
3952
+ const a = stored;
3953
+ const b = fresh;
3954
+ const aProps = a.properties ?? {};
3955
+ const bProps = b.properties ?? {};
3956
+ const aReq = new Set(a.required ?? []);
3957
+ const bReq = new Set(b.required ?? []);
3958
+ const aKeys = Object.keys(aProps);
3959
+ const bKeys = Object.keys(bProps);
3960
+ const added = bKeys.filter((k) => !(k in aProps));
3961
+ const removed = aKeys.filter((k) => !(k in bProps));
3962
+ const changed = [];
3963
+ for (const k of bKeys) {
3964
+ if (!(k in aProps)) continue;
3965
+ const shapeChanged = canonicalize(aProps[k]) !== canonicalize(bProps[k]);
3966
+ const requiredChanged = aReq.has(k) !== bReq.has(k);
3967
+ if (shapeChanged || requiredChanged) {
3968
+ changed.push({ field: k, requiredChanged, shapeChanged });
3969
+ }
3970
+ }
3971
+ let kind;
3972
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
3973
+ kind = "none";
3974
+ } else if (removed.length === 0 && changed.length === 0 && added.every((k) => !bReq.has(k))) {
3975
+ kind = "additive";
3976
+ } else {
3977
+ kind = "non-additive";
3978
+ }
3979
+ return { collection, kind, added, removed, changed };
3980
+ }
3981
+
3982
+ // src/schema-update/dispatch.ts
3983
+ async function evaluateStrategies(delta, strategies, ctx) {
3984
+ for (const strategy of strategies) {
3985
+ const decision = await strategy.onSchemaDelta(delta, ctx);
3986
+ if (decision.action !== "allow") return decision;
3987
+ }
3988
+ return { action: "allow" };
3989
+ }
3990
+
3805
3991
  // src/persisted-schemas/register.ts
3806
3992
  async function persistSchemaIfNeeded(opts) {
3807
3993
  const fresh = await derivePersistedSchema(opts.validator);
3808
3994
  const stored = await loadPersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek);
3809
3995
  if (stored && isEquivalent(stored, fresh)) {
3810
- return { written: false, skipped: true, envelope: stored };
3996
+ return { written: false, skipped: true, envelope: stored, decision: { action: "allow" } };
3997
+ }
3998
+ let decision = { action: "allow" };
3999
+ const strategies = opts.strategies ?? [];
4000
+ if (stored && strategies.length > 0 && stored.kind === fresh.kind && isPlainObject2(stored.jsonSchema) && isPlainObject2(fresh.jsonSchema)) {
4001
+ const delta = computeSchemaDelta(stored.jsonSchema, fresh.jsonSchema, opts.collectionName);
4002
+ decision = await evaluateStrategies(delta, strategies, { collection: opts.collectionName });
4003
+ }
4004
+ if (decision.action !== "allow") {
4005
+ return { written: false, skipped: false, envelope: stored ?? fresh, decision };
3811
4006
  }
3812
4007
  await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, fresh);
3813
- return { written: true, skipped: false, envelope: fresh };
4008
+ return { written: true, skipped: false, envelope: fresh, decision };
4009
+ }
4010
+ function isPlainObject2(v) {
4011
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3814
4012
  }
3815
4013
  function isEquivalent(a, b) {
3816
4014
  if (a.kind !== b.kind) return false;
@@ -3819,6 +4017,254 @@ function isEquivalent(a, b) {
3819
4017
  return false;
3820
4018
  }
3821
4019
 
4020
+ // src/schema-update/gate.ts
4021
+ var SchemaUpdateGate = class {
4022
+ #decision;
4023
+ constructor(decision) {
4024
+ this.#decision = decision.catch(() => null);
4025
+ }
4026
+ async assertWritable() {
4027
+ const decision = await this.#decision;
4028
+ if (decision && decision.action === "reject") {
4029
+ throw decision.error;
4030
+ }
4031
+ }
4032
+ };
4033
+
4034
+ // src/schema-update/fence.ts
4035
+ var FENCE_RECORD_ID = "schema-fence";
4036
+ var META_COLLECTION2 = "_meta";
4037
+ var DEFAULT_FENCE = { currentSchemaVersion: 0, fenceState: "normal" };
4038
+ async function loadFence(store, vault) {
4039
+ const envelope = await store.get(vault, META_COLLECTION2, FENCE_RECORD_ID);
4040
+ if (!envelope) return DEFAULT_FENCE;
4041
+ try {
4042
+ const parsed = JSON.parse(envelope._data);
4043
+ if (!isFenceDoc(parsed)) return DEFAULT_FENCE;
4044
+ return parsed;
4045
+ } catch {
4046
+ return DEFAULT_FENCE;
4047
+ }
4048
+ }
4049
+ async function saveFence(store, vault, fence) {
4050
+ const envelope = {
4051
+ _noydb: NOYDB_FORMAT_VERSION,
4052
+ _v: 1,
4053
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
4054
+ _iv: "",
4055
+ _data: JSON.stringify(fence)
4056
+ };
4057
+ await store.put(vault, META_COLLECTION2, FENCE_RECORD_ID, envelope);
4058
+ }
4059
+ function isFenceDoc(x) {
4060
+ if (x === null || typeof x !== "object") return false;
4061
+ const o = x;
4062
+ return typeof o["currentSchemaVersion"] === "number" && (o["fenceState"] === "normal" || o["fenceState"] === "draining" || o["fenceState"] === "migrating" || o["fenceState"] === "complete");
4063
+ }
4064
+
4065
+ // src/schema-update/client-registry.ts
4066
+ var META_COLLECTION3 = "_meta";
4067
+ var CLIENT_PREFIX = "schema-fence:client:";
4068
+ async function writeClientDoc(store, vault, clientId, doc) {
4069
+ const envelope = {
4070
+ _noydb: NOYDB_FORMAT_VERSION,
4071
+ _v: 1,
4072
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
4073
+ _iv: "",
4074
+ _data: JSON.stringify({ clientId, ...doc })
4075
+ };
4076
+ await store.put(vault, META_COLLECTION3, `${CLIENT_PREFIX}${clientId}`, envelope);
4077
+ }
4078
+ async function listClientDocs(store, vault) {
4079
+ const ids = await store.list(vault, META_COLLECTION3);
4080
+ const out = [];
4081
+ for (const id of ids) {
4082
+ if (!id.startsWith(CLIENT_PREFIX)) continue;
4083
+ const env = await store.get(vault, META_COLLECTION3, id);
4084
+ if (!env) continue;
4085
+ try {
4086
+ const parsed = JSON.parse(env._data);
4087
+ if (isClientDoc(parsed)) out.push(parsed);
4088
+ } catch {
4089
+ }
4090
+ }
4091
+ return out;
4092
+ }
4093
+ async function activeQuiesced(store, vault, opts) {
4094
+ const docs = await listClientDocs(store, vault);
4095
+ const active = docs.filter(
4096
+ (d) => d.lastSeen >= opts.now - opts.staleMs && d.clientId !== opts.excludeClientId
4097
+ );
4098
+ return active.every((d) => d.quiescedAtVersion === opts.generation);
4099
+ }
4100
+ function isClientDoc(x) {
4101
+ if (x === null || typeof x !== "object") return false;
4102
+ const o = x;
4103
+ return typeof o["clientId"] === "string" && typeof o["lastSeen"] === "number" && (o["quiescedAtVersion"] === null || typeof o["quiescedAtVersion"] === "number");
4104
+ }
4105
+
4106
+ // src/schema-update/fence-controller.ts
4107
+ var SchemaFenceController = class {
4108
+ #store;
4109
+ #vault;
4110
+ #onFlush;
4111
+ #clientId;
4112
+ #now;
4113
+ #staleMs;
4114
+ #quiesceTimeoutMs;
4115
+ #emit;
4116
+ #snapshot = 0;
4117
+ #pending = /* @__PURE__ */ new Map();
4118
+ constructor(opts) {
4119
+ this.#store = opts.store;
4120
+ this.#vault = opts.vault;
4121
+ this.#onFlush = opts.onFlush;
4122
+ this.#clientId = opts.clientId ?? "migrator";
4123
+ this.#now = opts.now ?? (() => Date.now());
4124
+ this.#staleMs = opts.staleMs ?? 3e4;
4125
+ this.#quiesceTimeoutMs = opts.quiesceTimeoutMs ?? 6e4;
4126
+ this.#emit = opts.emit ?? (() => {
4127
+ });
4128
+ }
4129
+ /** Capture the generation snapshot at vault-open. */
4130
+ async init() {
4131
+ this.#snapshot = (await loadFence(this.#store, this.#vault)).currentSchemaVersion;
4132
+ }
4133
+ /** Record a per-collection pending cutover (from a registration `cutover` decision). */
4134
+ registerPendingCutover(collection, transform) {
4135
+ this.#pending.set(collection, transform);
4136
+ }
4137
+ /** Write-path gate. Throws when behind, fenced, or this collection is cutover-pending. */
4138
+ async assertWritable(collection) {
4139
+ const fence = await loadFence(this.#store, this.#vault);
4140
+ if (fence.currentSchemaVersion > this.#snapshot) {
4141
+ throw new MigrationRequiredError(
4142
+ `Vault "${this.#vault}" advanced to schema generation ${fence.currentSchemaVersion} (this client opened at ${this.#snapshot}). Reload to continue.`
4143
+ );
4144
+ }
4145
+ if (fence.fenceState === "draining" || fence.fenceState === "migrating") {
4146
+ throw new SchemaFenceError(`Vault "${this.#vault}" is mid-cutover (${fence.fenceState}); writes are paused.`);
4147
+ }
4148
+ if (this.#pending.has(collection)) {
4149
+ throw new SchemaFenceError(
4150
+ `Collection "${collection}" has a pending schema cutover; run vault.runSchemaCutover() before writing.`
4151
+ );
4152
+ }
4153
+ }
4154
+ /**
4155
+ * Admin trigger. Drain → wait for the active set to quiesce (or time out)
4156
+ * → migrate each pending transform → bump → complete → normal. The
4157
+ * migrator excludes itself from the barrier (it drained synchronously
4158
+ * here). `onPoll` (tests) advances other clients between barrier checks;
4159
+ * production falls back to a short real delay.
4160
+ */
4161
+ async runCutover(run, opts) {
4162
+ if (this.#pending.size === 0) return { migrated: 0 };
4163
+ const base = await loadFence(this.#store, this.#vault);
4164
+ const generation = base.currentSchemaVersion;
4165
+ await this.#setState(generation, "draining");
4166
+ await this.#onFlush();
4167
+ const deadline = this.#now() + this.#quiesceTimeoutMs;
4168
+ while (!await activeQuiesced(this.#store, this.#vault, {
4169
+ generation,
4170
+ now: this.#now(),
4171
+ staleMs: this.#staleMs,
4172
+ excludeClientId: this.#clientId
4173
+ })) {
4174
+ if (this.#now() >= deadline) {
4175
+ throw new QuiesceTimeoutError(
4176
+ `Cutover on "${this.#vault}" timed out waiting for clients to quiesce at generation ${generation}.`
4177
+ );
4178
+ }
4179
+ await (opts?.onPoll ? opts.onPoll() : delay(50));
4180
+ }
4181
+ await this.#setState(generation, "migrating");
4182
+ let migrated = 0;
4183
+ for (const [collection, transform] of this.#pending) {
4184
+ await run(collection, transform);
4185
+ migrated++;
4186
+ }
4187
+ const nextVersion = generation + 1;
4188
+ await this.#setState(nextVersion, "complete");
4189
+ this.#pending.clear();
4190
+ await this.#setState(nextVersion, "normal");
4191
+ this.#snapshot = nextVersion;
4192
+ return { migrated };
4193
+ }
4194
+ /** Recover a stuck drain: reset fenceState to normal at the current version (no bump). */
4195
+ async abort() {
4196
+ const fence = await loadFence(this.#store, this.#vault);
4197
+ await this.#setState(fence.currentSchemaVersion, "normal");
4198
+ }
4199
+ async #setState(currentSchemaVersion, fenceState) {
4200
+ await saveFence(this.#store, this.#vault, { currentSchemaVersion, fenceState });
4201
+ this.#emit({ currentSchemaVersion, fenceState });
4202
+ }
4203
+ };
4204
+ function delay(ms) {
4205
+ return new Promise((resolve) => setTimeout(resolve, ms));
4206
+ }
4207
+
4208
+ // src/schema-update/fence-watcher.ts
4209
+ var FenceWatcher = class {
4210
+ #store;
4211
+ #vault;
4212
+ #clientId;
4213
+ #onFlush;
4214
+ #now;
4215
+ #emit;
4216
+ #lastState = null;
4217
+ #quiescedAtVersion = null;
4218
+ #timer;
4219
+ constructor(opts) {
4220
+ this.#store = opts.store;
4221
+ this.#vault = opts.vault;
4222
+ this.#clientId = opts.clientId;
4223
+ this.#onFlush = opts.onFlush;
4224
+ this.#now = opts.now ?? (() => Date.now());
4225
+ this.#emit = opts.emit ?? (() => {
4226
+ });
4227
+ }
4228
+ /** Publish liveness (and the current ack) without changing quiesce state. */
4229
+ async beat() {
4230
+ await writeClientDoc(this.#store, this.#vault, this.#clientId, {
4231
+ lastSeen: this.#now(),
4232
+ quiescedAtVersion: this.#quiescedAtVersion
4233
+ });
4234
+ }
4235
+ /** Poll the fence; quiesce on draining; emit on transitions. */
4236
+ async check() {
4237
+ const fence = await loadFence(this.#store, this.#vault);
4238
+ if (fence.fenceState !== this.#lastState) {
4239
+ this.#lastState = fence.fenceState;
4240
+ this.#emit({ currentSchemaVersion: fence.currentSchemaVersion, fenceState: fence.fenceState });
4241
+ }
4242
+ if (fence.fenceState === "draining" && this.#quiescedAtVersion !== fence.currentSchemaVersion) {
4243
+ await this.#onFlush();
4244
+ this.#quiescedAtVersion = fence.currentSchemaVersion;
4245
+ await this.beat();
4246
+ }
4247
+ if (fence.fenceState === "normal") {
4248
+ this.#quiescedAtVersion = null;
4249
+ }
4250
+ }
4251
+ start(intervalMs) {
4252
+ if (this.#timer) return;
4253
+ this.#timer = setInterval(() => {
4254
+ void this.beat();
4255
+ void this.check();
4256
+ }, intervalMs);
4257
+ const timer = this.#timer;
4258
+ if (typeof timer.unref === "function") timer.unref();
4259
+ }
4260
+ stop() {
4261
+ if (this.#timer) {
4262
+ clearInterval(this.#timer);
4263
+ this.#timer = void 0;
4264
+ }
4265
+ }
4266
+ };
4267
+
3822
4268
  // src/introspection/fields.ts
3823
4269
  function jsonSchemaType(node) {
3824
4270
  if (Array.isArray(node.type)) {
@@ -4165,6 +4611,13 @@ var Vault = class {
4165
4611
  */
4166
4612
  reloadKeyring;
4167
4613
  collectionCache = /* @__PURE__ */ new Map();
4614
+ /** #232 — vault-level schema cutover fence/controller. */
4615
+ schemaFence;
4616
+ /** #232 — per-client heartbeat/watcher; started lazily on cutover registration. */
4617
+ #fenceWatcher;
4618
+ #fenceCoordinationStarted = false;
4619
+ /** #229 — per-collection registered schema-update strategy names. */
4620
+ #schemaUpdateNames = /* @__PURE__ */ new Map();
4168
4621
  /**
4169
4622
  * per-collection `blobFields` retention/TTL config.
4170
4623
  * Populated on `collection({ blobFields })` and read by
@@ -4280,6 +4733,13 @@ var Vault = class {
4280
4733
  this.noydb = opts.noydb;
4281
4734
  this.keyring = opts.keyring;
4282
4735
  this.encrypted = opts.encrypted;
4736
+ this.schemaFence = new SchemaFenceController({
4737
+ store: this.adapter,
4738
+ vault: this.name,
4739
+ onFlush: () => this.noydb._writeQueueTracker.onFlush(),
4740
+ clientId: this.noydb._clientId,
4741
+ emit: (e) => this.emitter.emit("schema:fence-changed", { vault: this.name, ...e })
4742
+ });
4283
4743
  this.emitter = opts.emitter;
4284
4744
  this.onDirty = opts.onDirty;
4285
4745
  this.onRegisterConflictResolver = opts.onRegisterConflictResolver;
@@ -4388,6 +4848,35 @@ var Vault = class {
4388
4848
  }
4389
4849
  this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
4390
4850
  }
4851
+ if ((options?.schemaUpdate?.length ?? 0) > 0) {
4852
+ this.#schemaUpdateNames.set(collectionName, (options.schemaUpdate ?? []).map((s) => s.name));
4853
+ }
4854
+ let schemaUpdateGate;
4855
+ if (options?.persistJsonSchema === true && options.schema !== void 0 && (options.schemaUpdate?.length ?? 0) > 0) {
4856
+ const validator = options.schema;
4857
+ const strategies = options.schemaUpdate ?? [];
4858
+ const work = (async () => {
4859
+ const dek = await this.getDEK(collectionName);
4860
+ const result = await persistSchemaIfNeeded({
4861
+ store: this.adapter,
4862
+ vault: this.name,
4863
+ collectionName,
4864
+ validator,
4865
+ dek,
4866
+ strategies
4867
+ });
4868
+ const decision = result.decision ?? { action: "allow" };
4869
+ if (decision.action === "cutover") {
4870
+ this.schemaFence.registerPendingCutover(collectionName, decision.transform);
4871
+ this._ensureFenceCoordination();
4872
+ }
4873
+ return decision;
4874
+ })();
4875
+ this._pendingSchemaWrites.push(work.then(() => {
4876
+ }, () => {
4877
+ }));
4878
+ schemaUpdateGate = new SchemaUpdateGate(work);
4879
+ }
4391
4880
  const collOpts = {
4392
4881
  adapter: this.adapter,
4393
4882
  vault: this.name,
@@ -4395,6 +4884,11 @@ var Vault = class {
4395
4884
  keyring: this.keyring,
4396
4885
  encrypted: this.encrypted,
4397
4886
  emitter: this.emitter,
4887
+ writeQueue: this.noydb._writeQueueTracker,
4888
+ writeHooks: this.noydb._writeHooks,
4889
+ activeTxId: () => this.noydb._activeTxContextOrNull?.txId ?? null,
4890
+ schemaUpdateGate,
4891
+ schemaFence: this.schemaFence,
4398
4892
  getDEK: this.getDEK,
4399
4893
  onDirty: this.onDirty,
4400
4894
  historyConfig: this.historyConfig,
@@ -4481,7 +4975,7 @@ var Vault = class {
4481
4975
  }
4482
4976
  coll = new Collection(collOpts);
4483
4977
  this.collectionCache.set(collectionName, coll);
4484
- if (options?.persistJsonSchema === true && options.schema !== void 0) {
4978
+ if (options?.persistJsonSchema === true && options.schema !== void 0 && (options.schemaUpdate?.length ?? 0) === 0) {
4485
4979
  const validator = options.schema;
4486
4980
  const work = (async () => {
4487
4981
  try {
@@ -4514,6 +5008,87 @@ var Vault = class {
4514
5008
  this._pendingSchemaWrites = [];
4515
5009
  await Promise.allSettled(pending);
4516
5010
  }
5011
+ /**
5012
+ * Run a coordinated schema cutover (#232). Drains pending writes, waits
5013
+ * for the active client set to quiesce (the ack-barrier), applies every
5014
+ * pending collection transform in bulk, bumps the vault schema generation,
5015
+ * and clears the fence. Returns the count of collections migrated.
5016
+ * `opts.onPoll` (tests) advances other clients between barrier checks.
5017
+ */
5018
+ async runSchemaCutover(opts) {
5019
+ return this.schemaFence.runCutover(
5020
+ (collectionName, transform) => this.#runCutoverTransform(collectionName, transform),
5021
+ opts
5022
+ );
5023
+ }
5024
+ async #runCutoverTransform(collectionName, transform) {
5025
+ const coll = this.collectionCache.get(collectionName);
5026
+ if (!coll) return;
5027
+ await coll._applyCutoverTransform(transform);
5028
+ }
5029
+ /**
5030
+ * #228b — refresh a loaded collection's view of one document from a peer
5031
+ * tab's broadcast. No-op when the collection isn't loaded in this tab
5032
+ * (it will read fresh on next open). Mirrors #runCutoverTransform's guard.
5033
+ */
5034
+ async _applyRemoteWrite(collectionName, docId, action) {
5035
+ const coll = this.collectionCache.get(collectionName);
5036
+ if (!coll) return;
5037
+ await coll._applyRemoteChange(docId, action);
5038
+ }
5039
+ /**
5040
+ * #228c — for a detected conflict: capture this tab's clobbered record,
5041
+ * read the common ancestor from history, converge the cache to the store's
5042
+ * authoritative value (the (b) re-read), and return all three for the
5043
+ * WriteConflict payload. Returns null when the collection isn't loaded.
5044
+ */
5045
+ async _captureAndConverge(collectionName, docId, action, baseV) {
5046
+ const coll = this.collectionCache.get(collectionName);
5047
+ if (!coll) return null;
5048
+ const local = coll._peekCached(docId);
5049
+ let base = null;
5050
+ try {
5051
+ base = await coll.getVersion(docId, baseV);
5052
+ } catch {
5053
+ base = null;
5054
+ }
5055
+ await coll._applyRemoteChange(docId, action);
5056
+ const remote = await coll.get(docId);
5057
+ return { local, remote, base };
5058
+ }
5059
+ /** Recover a stuck cutover fence (#232) — reset to normal without bumping. */
5060
+ async abortSchemaCutover() {
5061
+ await this.schemaFence.abort();
5062
+ }
5063
+ /** Current schema-cutover fence state for this vault (#232/#233). Thin live read. */
5064
+ async schemaFenceState() {
5065
+ return loadFence(this.adapter, this.name);
5066
+ }
5067
+ /** @internal Start the per-client heartbeat + fence watcher once a cutover is registered (#232). */
5068
+ _ensureFenceCoordination() {
5069
+ if (this.#fenceCoordinationStarted) return;
5070
+ this.#fenceCoordinationStarted = true;
5071
+ this.#fenceWatcher = new FenceWatcher({
5072
+ store: this.adapter,
5073
+ vault: this.name,
5074
+ clientId: this.noydb._clientId,
5075
+ onFlush: () => this.noydb._writeQueueTracker.onFlush(),
5076
+ emit: (e) => this.emitter.emit("schema:fence-changed", { vault: this.name, ...e })
5077
+ });
5078
+ this.#fenceWatcher.start(2e3);
5079
+ }
5080
+ /** @internal Stop the heartbeat/watcher (vault lock/close). */
5081
+ _stopFenceCoordination() {
5082
+ this.#fenceWatcher?.stop();
5083
+ this.#fenceWatcher = void 0;
5084
+ this.#fenceCoordinationStarted = false;
5085
+ }
5086
+ /** @internal Drive one heartbeat + watch cycle deterministically (tests). */
5087
+ async _fenceTick() {
5088
+ this._ensureFenceCoordination();
5089
+ await this.#fenceWatcher.beat();
5090
+ await this.#fenceWatcher.check();
5091
+ }
4517
5092
  /**
4518
5093
  * Validate i18nText fields on a `put()`. Called by Collection just
4519
5094
  * before the adapter write, after schema validation. Throws
@@ -4839,12 +5414,12 @@ var Vault = class {
4839
5414
  if (!fieldSchema) {
4840
5415
  throw new AttestationError(`issueAttestation: collection '${collectionName}' has no attestation field-schema. Declare it via vault.collection('${collectionName}', { attestation: { fields: [...] } }).`);
4841
5416
  }
4842
- const { issueAttestationCore } = await import("./issue-ORP37MVW.js");
5417
+ const { issueAttestationCore } = await import("./issue-BAJ7ZB4S.js");
4843
5418
  const out = await issueAttestationCore(this.makeIssueContext(), { collection: collectionName, id, fieldSchema });
4844
5419
  return { docId: out.docId, qr: out.qr, keyId: out.keyId, publicKeyB64: out.publicKeyB64 };
4845
5420
  }
4846
5421
  async getDocumentSigningPublicKey() {
4847
- const { loadSigner, loadOrCreateSigner } = await import("./signer-GRI5TZKH.js");
5422
+ const { loadSigner, loadOrCreateSigner } = await import("./signer-M4K5HBLD.js");
4848
5423
  const existing = await loadSigner(this.adapter, this.name, this.getDEK);
4849
5424
  if (existing) return { keyId: existing.keyId, publicKeyB64: existing.publicKeyB64 };
4850
5425
  if (this.keyring.role !== "owner") {
@@ -4870,19 +5445,19 @@ var Vault = class {
4870
5445
  };
4871
5446
  }
4872
5447
  async revokeAttestation(docId) {
4873
- const { revokeDocCore } = await import("./revoke-KY2GB4KP.js");
5448
+ const { revokeDocCore } = await import("./revoke-7JOVLZFD.js");
4874
5449
  await revokeDocCore(this.makeRevokeContext(), docId);
4875
5450
  }
4876
5451
  async unrevokeAttestation(docId) {
4877
- const { unrevokeDocCore } = await import("./revoke-KY2GB4KP.js");
5452
+ const { unrevokeDocCore } = await import("./revoke-7JOVLZFD.js");
4878
5453
  await unrevokeDocCore(this.makeRevokeContext(), docId);
4879
5454
  }
4880
5455
  async getRevokedDocIds() {
4881
- const { getRevokedDocIdsCore } = await import("./revoke-KY2GB4KP.js");
5456
+ const { getRevokedDocIdsCore } = await import("./revoke-7JOVLZFD.js");
4882
5457
  return getRevokedDocIdsCore(this.makeRevokeContext());
4883
5458
  }
4884
5459
  async publishRevocationList() {
4885
- const { publishRevocationListCore } = await import("./revoke-KY2GB4KP.js");
5460
+ const { publishRevocationListCore } = await import("./revoke-7JOVLZFD.js");
4886
5461
  return publishRevocationListCore(this.makeRevokeContext());
4887
5462
  }
4888
5463
  makeRevokeContext() {
@@ -5171,7 +5746,7 @@ var Vault = class {
5171
5746
  async _initGuards(handles) {
5172
5747
  if (handles.length === 0) return;
5173
5748
  const [{ GuardRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
5174
- import("./registry-RFGGMVNJ.js"),
5749
+ import("./registry-2IEARCGT.js"),
5175
5750
  import("./read-only-facade-ITU6L7BL.js")
5176
5751
  ]);
5177
5752
  const registry = new GuardRegistry();
@@ -5200,7 +5775,7 @@ var Vault = class {
5200
5775
  async _initDerivations(handles) {
5201
5776
  if (handles.length === 0) return;
5202
5777
  const [{ DerivationRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
5203
- import("./registry-PSIPG2QR.js"),
5778
+ import("./registry-EMGLZGR6.js"),
5204
5779
  import("./read-only-facade-ITU6L7BL.js")
5205
5780
  ]);
5206
5781
  const registry = new DerivationRegistry();
@@ -5231,7 +5806,7 @@ var Vault = class {
5231
5806
  */
5232
5807
  async _initMaterializedViews(handles) {
5233
5808
  if (handles.length === 0) return;
5234
- const { MaterializedViewRegistry } = await import("./registry-3ALP62P6.js");
5809
+ const { MaterializedViewRegistry } = await import("./registry-CDHASH73.js");
5235
5810
  const registry = new MaterializedViewRegistry();
5236
5811
  this.materializedViewRegistry = registry;
5237
5812
  const db = this;
@@ -5255,7 +5830,7 @@ var Vault = class {
5255
5830
  */
5256
5831
  async _initOverlayedViews(handles) {
5257
5832
  if (handles.length === 0) return;
5258
- const { OverlayedViewRegistry } = await import("./registry-7HE6VJGC.js");
5833
+ const { OverlayedViewRegistry } = await import("./registry-NQALYR77.js");
5259
5834
  const registry = new OverlayedViewRegistry();
5260
5835
  const mvRegistry = this.materializedViewRegistry;
5261
5836
  const overlayNames = /* @__PURE__ */ new Set();
@@ -5302,13 +5877,13 @@ var Vault = class {
5302
5877
  if (!reg) {
5303
5878
  throw new Error(`refreshView: no MV registered with name "${name}"`);
5304
5879
  }
5305
- const { MaterializedViewExecutor } = await import("./executor-AS2IDHKZ.js");
5880
+ const { MaterializedViewExecutor } = await import("./executor-KT2IOZVP.js");
5306
5881
  const result = await MaterializedViewExecutor.refresh(reg, {
5307
5882
  getCollection: (n) => this.collection(n),
5308
5883
  getActiveTxContext: () => this.noydb._activeTxContextOrNull,
5309
5884
  getQueryContext: () => this
5310
5885
  });
5311
- const { clearMVStale } = await import("./stale-OTOF3FH7.js");
5886
+ const { clearMVStale } = await import("./stale-PAGCS4K5.js");
5312
5887
  clearMVStale(registry, name);
5313
5888
  return result;
5314
5889
  }
@@ -5324,7 +5899,7 @@ var Vault = class {
5324
5899
  if (registry === null) return { derived: 0, failed: 0 };
5325
5900
  const strategies = registry.strategiesForSource(sourceCollection);
5326
5901
  if (strategies.length === 0) return { derived: 0, failed: 0 };
5327
- const { DerivationExecutor } = await import("./executor-HLXFXNFM.js");
5902
+ const { DerivationExecutor } = await import("./executor-GFZFDQXV.js");
5328
5903
  const sourceColl = this.collection(sourceCollection);
5329
5904
  const records = await sourceColl.list();
5330
5905
  const ctx = { vault: this.readOnlyFacade ?? new (await import("./read-only-facade-ITU6L7BL.js")).ReadOnlyVaultFacade(this) };
@@ -5349,7 +5924,7 @@ var Vault = class {
5349
5924
  if (!outSpec) continue;
5350
5925
  const outputColl = this.collection(outSpec.collection);
5351
5926
  if (out.kind === "array") {
5352
- const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-VJ52RIEY.js");
5927
+ const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-NRBWSLRK.js");
5353
5928
  const prior = await loadFanoutSidecar(this.adapter, this.name, spec.source, id, key);
5354
5929
  const prevKeys = new Set(prior?.keys ?? []);
5355
5930
  const newKeysList = out.entries.map((e) => e.key);
@@ -5573,7 +6148,7 @@ var Vault = class {
5573
6148
  * collection.
5574
6149
  */
5575
6150
  async delegate(opts) {
5576
- const { issueDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-QYXZW25W.js");
6151
+ const { issueDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-QSC7G5QC.js");
5577
6152
  if (!this.keyring.kek) {
5578
6153
  throw new ValidationError(
5579
6154
  "issueDelegation: keyring.kek is null \u2014 issuing a delegation requires a tier-1 unlock. Re-authenticate at tier 1 (passphrase) first."
@@ -5595,7 +6170,7 @@ var Vault = class {
5595
6170
  * if the id does not exist.
5596
6171
  */
5597
6172
  async revokeDelegation(id) {
5598
- const { revokeDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-QYXZW25W.js");
6173
+ const { revokeDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-QSC7G5QC.js");
5599
6174
  await revokeDelegation(this.adapter, this.name, id);
5600
6175
  void DELEGATIONS_COLLECTION;
5601
6176
  }
@@ -5942,6 +6517,27 @@ var Vault = class {
5942
6517
  async dumpSchema(opts = {}) {
5943
6518
  return dumpVaultSchema(this, opts);
5944
6519
  }
6520
+ /**
6521
+ * Lightweight read of the vault's registered schema (#229): collections
6522
+ * (+ doc counts), guards, materialized views, schema-update strategies,
6523
+ * and the unlocked user's grants. Cheap — one `adapter.list` per
6524
+ * collection, no decryption. For a full snapshot + stats use dumpSchema().
6525
+ * Post-unlock by construction (a Vault only exists with an unlocked keyring).
6526
+ */
6527
+ async introspect() {
6528
+ const byCol = (a, b) => a.collection.localeCompare(b.collection);
6529
+ const names = [.../* @__PURE__ */ new Set([...this.collectionCache.keys(), ...await this.collections()])].filter((n) => !n.startsWith("_")).sort((a, b) => a.localeCompare(b));
6530
+ const collections = [];
6531
+ for (const name of names) {
6532
+ const ids = await this.adapter.list(this.name, name);
6533
+ collections.push({ name, docCount: ids.length });
6534
+ }
6535
+ const guards = (this._getGuardRegistry()?.summary() ?? []).slice().sort(byCol);
6536
+ const materializedViews = (this._getMaterializedViewRegistry()?.all() ?? []).map((mv) => ({ name: mv.spec.name, sourceCollections: [...mv.dependencies].sort() })).sort((a, b) => a.name.localeCompare(b.name));
6537
+ const schemaUpdate = [...this.#schemaUpdateNames.entries()].map(([collection, strategies]) => ({ collection, strategies })).sort(byCol);
6538
+ const grants = [...this.keyring.deks.keys()].filter((collection) => !collection.startsWith("_")).map((collection) => ({ collection, permission: this.keyring.permissions[collection] ?? "rw" })).sort(byCol);
6539
+ return { collections, guards, materializedViews, schemaUpdate, grants };
6540
+ }
5945
6541
  /**
5946
6542
  * Internal accessor for {@link dumpVaultSchema}. Exposes the structural
5947
6543
  * state the walker needs (collection cache, registries, ref registry,
@@ -6043,7 +6639,7 @@ var Vault = class {
6043
6639
  * @see docs/subsystems/public-envelope.md
6044
6640
  */
6045
6641
  async getPublicEnvelope(opts = {}) {
6046
- const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-U3CMEOMV.js");
6642
+ const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-OHQ5UZFM.js");
6047
6643
  return readPublicEnvelope2(this.adapter, this.name, opts);
6048
6644
  }
6049
6645
  /**
@@ -6581,6 +7177,387 @@ var NoydbEventEmitter = class {
6581
7177
  }
6582
7178
  };
6583
7179
 
7180
+ // src/write-queue.ts
7181
+ var WriteQueueTracker = class {
7182
+ #depth = 0;
7183
+ #error = null;
7184
+ #changeHandlers = /* @__PURE__ */ new Set();
7185
+ #flushWaiters = [];
7186
+ get pending() {
7187
+ return this.#depth > 0;
7188
+ }
7189
+ get depth() {
7190
+ return this.#depth;
7191
+ }
7192
+ /** Mark one write as started. */
7193
+ begin() {
7194
+ this.#depth++;
7195
+ this.#emitChange();
7196
+ }
7197
+ /** Mark one write as finished. Pass the error if it failed. */
7198
+ settle(error) {
7199
+ this.#depth = Math.max(0, this.#depth - 1);
7200
+ if (error) this.#error = error;
7201
+ this.#emitChange();
7202
+ if (this.#depth === 0) this.#drainFlush();
7203
+ }
7204
+ onChange(handler) {
7205
+ this.#changeHandlers.add(handler);
7206
+ return () => {
7207
+ this.#changeHandlers.delete(handler);
7208
+ };
7209
+ }
7210
+ onFlush() {
7211
+ if (this.#depth === 0) {
7212
+ const error = this.#error;
7213
+ this.#error = null;
7214
+ return error ? Promise.reject(error) : Promise.resolve();
7215
+ }
7216
+ return new Promise((resolve, reject) => {
7217
+ this.#flushWaiters.push({ resolve, reject });
7218
+ });
7219
+ }
7220
+ /**
7221
+ * Run `fn` as a tracked write: depth++ on entry, depth-- on settle
7222
+ * (success or failure). The fn's resolved value is returned; a thrown
7223
+ * error is re-thrown after the queue is decremented.
7224
+ */
7225
+ async track(fn) {
7226
+ this.begin();
7227
+ try {
7228
+ const value = await fn();
7229
+ this.settle();
7230
+ return value;
7231
+ } catch (error) {
7232
+ this.settle(error);
7233
+ throw error;
7234
+ }
7235
+ }
7236
+ #emitChange() {
7237
+ for (const handler of this.#changeHandlers) handler();
7238
+ }
7239
+ #drainFlush() {
7240
+ const waiters = this.#flushWaiters;
7241
+ this.#flushWaiters = [];
7242
+ const error = this.#error;
7243
+ this.#error = null;
7244
+ for (const waiter of waiters) {
7245
+ if (error) waiter.reject(error);
7246
+ else waiter.resolve();
7247
+ }
7248
+ }
7249
+ };
7250
+
7251
+ // src/write-hooks.ts
7252
+ var WriteHookRegistry = class {
7253
+ #before = [];
7254
+ #after = [];
7255
+ #suppressed = false;
7256
+ /** True while handlers are running — used by the write path to skip nested firing. */
7257
+ get suppressed() {
7258
+ return this.#suppressed;
7259
+ }
7260
+ /** True when any hook is registered (cheap gate for the write path). */
7261
+ get hasHandlers() {
7262
+ return this.#before.length > 0 || this.#after.length > 0;
7263
+ }
7264
+ onBeforeWrite(handler) {
7265
+ this.#before.push(handler);
7266
+ return () => {
7267
+ const i = this.#before.indexOf(handler);
7268
+ if (i >= 0) this.#before.splice(i, 1);
7269
+ };
7270
+ }
7271
+ onAfterWrite(handler) {
7272
+ this.#after.push(handler);
7273
+ return () => {
7274
+ const i = this.#after.indexOf(handler);
7275
+ if (i >= 0) this.#after.splice(i, 1);
7276
+ };
7277
+ }
7278
+ /** Run before-hooks (awaited, in order). A throw propagates and aborts the write. */
7279
+ async runBefore(event) {
7280
+ if (this.#before.length === 0) return;
7281
+ this.#suppressed = true;
7282
+ try {
7283
+ for (const h of this.#before.slice()) await h(event);
7284
+ } finally {
7285
+ this.#suppressed = false;
7286
+ }
7287
+ }
7288
+ /** Run after-hooks (awaited, in order). Per-handler errors are warned, not thrown. */
7289
+ async runAfter(event) {
7290
+ if (this.#after.length === 0) return;
7291
+ this.#suppressed = true;
7292
+ try {
7293
+ for (const h of this.#after.slice()) {
7294
+ try {
7295
+ await h(event);
7296
+ } catch (err) {
7297
+ console.warn(
7298
+ `[noy-db] onAfterWrite handler failed for ${event.collection}/${event.docId}: ` + (err instanceof Error ? err.message : String(err))
7299
+ );
7300
+ }
7301
+ }
7302
+ } finally {
7303
+ this.#suppressed = false;
7304
+ }
7305
+ }
7306
+ };
7307
+
7308
+ // src/tab-coordination.ts
7309
+ var TabCoordinator = class {
7310
+ tabId;
7311
+ role = "unknown";
7312
+ #lockManager;
7313
+ #channel;
7314
+ #lockName;
7315
+ #heartbeatMs;
7316
+ #staleMs;
7317
+ #now;
7318
+ #peers = /* @__PURE__ */ new Map();
7319
+ #roleHandlers = /* @__PURE__ */ new Set();
7320
+ #tabsHandlers = /* @__PURE__ */ new Set();
7321
+ #ac;
7322
+ #releaseLock;
7323
+ #unsub;
7324
+ #closeUnsub;
7325
+ #timer;
7326
+ #ownsChannel;
7327
+ #started = false;
7328
+ #disposed = false;
7329
+ #lastTabsSig = "";
7330
+ constructor(opts = {}) {
7331
+ this.tabId = opts.tabId ?? `tab-${Math.trunc((opts.now ?? (() => 0))())}-${cheapRand()}`;
7332
+ this.#lockManager = opts.lockManager;
7333
+ this.#channel = opts.channel;
7334
+ this.#lockName = opts.lockName ?? "noydb:tab-primary";
7335
+ this.#heartbeatMs = opts.heartbeatMs ?? 2e3;
7336
+ this.#staleMs = opts.staleMs ?? 6e3;
7337
+ this.#now = opts.now ?? (() => Date.now());
7338
+ this.#ownsChannel = opts.closeChannelOnDispose ?? false;
7339
+ }
7340
+ start() {
7341
+ if (this.#disposed || this.#started) return;
7342
+ this.#started = true;
7343
+ if (this.#channel) {
7344
+ this.#unsub = this.#channel.on("message", (p) => this.#onMessage(p));
7345
+ this.#closeUnsub = this.#channel.on("close", () => this.#onChannelClose());
7346
+ this.#beat();
7347
+ this.#timer = setInterval(() => this.#tick(), this.#heartbeatMs);
7348
+ const t = this.#timer;
7349
+ if (typeof t.unref === "function") t.unref();
7350
+ }
7351
+ if (this.#lockManager) {
7352
+ this.#ac = new AbortController();
7353
+ this.#setRole("secondary");
7354
+ void this.#lockManager.request(this.#lockName, { mode: "exclusive", signal: this.#ac.signal }, () => {
7355
+ this.#setRole("primary");
7356
+ return new Promise((resolve) => {
7357
+ this.#releaseLock = resolve;
7358
+ });
7359
+ }).catch(() => {
7360
+ });
7361
+ }
7362
+ }
7363
+ activeTabs() {
7364
+ if (!this.#channel) return [];
7365
+ const cutoff = this.#now() - this.#staleMs;
7366
+ const self = { tabId: this.tabId, lastSeen: this.#now(), role: this.role };
7367
+ const out = [self, ...[...this.#peers.values()].filter((p) => p.lastSeen >= cutoff)];
7368
+ return out.sort((a, b) => a.tabId.localeCompare(b.tabId));
7369
+ }
7370
+ onTabRoleChange(fn) {
7371
+ this.#roleHandlers.add(fn);
7372
+ return () => this.#roleHandlers.delete(fn);
7373
+ }
7374
+ onActiveTabsChange(fn) {
7375
+ this.#tabsHandlers.add(fn);
7376
+ return () => this.#tabsHandlers.delete(fn);
7377
+ }
7378
+ dispose() {
7379
+ if (this.#disposed) return;
7380
+ this.#disposed = true;
7381
+ this.#releaseLock?.();
7382
+ this.#ac?.abort();
7383
+ if (this.#timer) {
7384
+ clearInterval(this.#timer);
7385
+ this.#timer = void 0;
7386
+ }
7387
+ this.#unsub?.();
7388
+ this.#closeUnsub?.();
7389
+ if (this.#ownsChannel) this.#channel?.close();
7390
+ this.#setRole("unknown");
7391
+ }
7392
+ /** @internal test seam — broadcast one heartbeat now. */
7393
+ _beat() {
7394
+ this.#beat();
7395
+ }
7396
+ #tick() {
7397
+ this.#prune();
7398
+ this.#emitTabs();
7399
+ this.#beat();
7400
+ }
7401
+ #beat() {
7402
+ if (this.#disposed) return;
7403
+ if (!this.#channel || !this.#channel.isOpen) return;
7404
+ const msg = { kind: "tab-presence", tabId: this.tabId, lastSeen: this.#now(), role: this.role };
7405
+ this.#channel.send(JSON.stringify(msg));
7406
+ }
7407
+ #onChannelClose() {
7408
+ if (this.#timer) {
7409
+ clearInterval(this.#timer);
7410
+ this.#timer = void 0;
7411
+ }
7412
+ this.#setRole("unknown");
7413
+ }
7414
+ #onMessage(payload) {
7415
+ let msg;
7416
+ try {
7417
+ msg = JSON.parse(payload);
7418
+ } catch {
7419
+ return;
7420
+ }
7421
+ if (!isPresenceMsg(msg) || msg.tabId === this.tabId) return;
7422
+ this.#peers.set(msg.tabId, { tabId: msg.tabId, lastSeen: msg.lastSeen, role: msg.role });
7423
+ this.#prune();
7424
+ this.#emitTabs();
7425
+ }
7426
+ #prune() {
7427
+ const cutoff = this.#now() - this.#staleMs;
7428
+ for (const [id, p] of this.#peers) if (p.lastSeen < cutoff) this.#peers.delete(id);
7429
+ }
7430
+ #setRole(role) {
7431
+ if (this.role === role) return;
7432
+ this.role = role;
7433
+ for (const h of this.#roleHandlers) h(role);
7434
+ this.#beat();
7435
+ this.#emitTabs();
7436
+ }
7437
+ #emitTabs() {
7438
+ const tabs = this.activeTabs();
7439
+ const sig = tabs.map((t) => `${t.tabId}:${t.role}`).join("|");
7440
+ if (sig === this.#lastTabsSig) return;
7441
+ this.#lastTabsSig = sig;
7442
+ for (const h of this.#tabsHandlers) h(tabs);
7443
+ }
7444
+ };
7445
+ function isPresenceMsg(x) {
7446
+ if (x === null || typeof x !== "object") return false;
7447
+ const o = x;
7448
+ return o["kind"] === "tab-presence" && typeof o["tabId"] === "string" && typeof o["lastSeen"] === "number" && (o["role"] === "primary" || o["role"] === "secondary" || o["role"] === "unknown");
7449
+ }
7450
+ function cheapRand() {
7451
+ const g = globalThis;
7452
+ return g.crypto?.randomUUID ? g.crypto.randomUUID().slice(0, 8) : "anon";
7453
+ }
7454
+ function defaultLockManager() {
7455
+ const nav = globalThis.navigator;
7456
+ return nav?.locks;
7457
+ }
7458
+ function defaultChannel(name = "noydb:tabs") {
7459
+ if (typeof globalThis.window === "undefined") return void 0;
7460
+ const Bc = globalThis.BroadcastChannel;
7461
+ if (!Bc) return void 0;
7462
+ const bc = new Bc(name);
7463
+ const msgListeners = /* @__PURE__ */ new Set();
7464
+ bc.onmessage = (e) => {
7465
+ for (const l of msgListeners) l(String(e.data));
7466
+ };
7467
+ return {
7468
+ isOpen: true,
7469
+ send(payload) {
7470
+ bc.postMessage(payload);
7471
+ },
7472
+ on(event, listener) {
7473
+ if (event === "message") {
7474
+ const l = listener;
7475
+ msgListeners.add(l);
7476
+ return () => msgListeners.delete(l);
7477
+ }
7478
+ return () => {
7479
+ };
7480
+ },
7481
+ close() {
7482
+ msgListeners.clear();
7483
+ bc.close();
7484
+ }
7485
+ };
7486
+ }
7487
+
7488
+ // src/tab-write-relay.ts
7489
+ var CrossTabWriteRelay = class {
7490
+ #channel;
7491
+ #writerId;
7492
+ #subscribeAfterWrite;
7493
+ #applyRemoteWrite;
7494
+ #reportConflict;
7495
+ #ledger = /* @__PURE__ */ new Map();
7496
+ #ownsChannel;
7497
+ #unsubMsg;
7498
+ #unsubWrite;
7499
+ #started = false;
7500
+ #disposed = false;
7501
+ constructor(opts) {
7502
+ this.#channel = opts.channel;
7503
+ this.#writerId = opts.writerId;
7504
+ this.#subscribeAfterWrite = opts.subscribeAfterWrite;
7505
+ this.#applyRemoteWrite = opts.applyRemoteWrite;
7506
+ this.#reportConflict = opts.reportConflict;
7507
+ this.#ownsChannel = opts.closeChannelOnDispose ?? false;
7508
+ }
7509
+ start() {
7510
+ if (this.#started || this.#disposed) return;
7511
+ this.#started = true;
7512
+ this.#unsubMsg = this.#channel.on("message", (p) => this.#onMessage(p));
7513
+ this.#unsubWrite = this.#subscribeAfterWrite((e) => this.#onLocalWrite(e));
7514
+ }
7515
+ dispose() {
7516
+ if (this.#disposed) return;
7517
+ this.#disposed = true;
7518
+ this.#unsubWrite?.();
7519
+ this.#unsubMsg?.();
7520
+ if (this.#ownsChannel) this.#channel.close();
7521
+ }
7522
+ #onLocalWrite(e) {
7523
+ if (this.#disposed || !this.#channel.isOpen) return;
7524
+ this.#ledger.set(ledgerKey(e.vault, e.collection, e.docId), e.version);
7525
+ const action = e.op === "delete" ? "delete" : "put";
7526
+ const msg = { kind: "tab-write", writerId: this.#writerId, vault: e.vault, collection: e.collection, docId: e.docId, action, baseV: e.baseVersion, v: e.version };
7527
+ this.#channel.send(JSON.stringify(msg));
7528
+ }
7529
+ #onMessage(payload) {
7530
+ if (this.#disposed) return;
7531
+ let msg;
7532
+ try {
7533
+ msg = JSON.parse(payload);
7534
+ } catch {
7535
+ return;
7536
+ }
7537
+ if (!isTabWriteMsg(msg) || msg.writerId === this.#writerId) return;
7538
+ const key = ledgerKey(msg.vault, msg.collection, msg.docId);
7539
+ const ownV = this.#ledger.get(key);
7540
+ if (ownV !== void 0 && msg.baseV < ownV && this.#reportConflict) {
7541
+ void Promise.resolve(this.#reportConflict(msg.vault, msg.collection, msg.docId, msg.action, msg.baseV, msg.v, ownV)).catch((err) => {
7542
+ console.warn(`[noy-db] cross-tab conflict report failed for ${msg.collection}/${msg.docId}: ` + (err instanceof Error ? err.message : String(err)));
7543
+ });
7544
+ return;
7545
+ }
7546
+ if (ownV !== void 0 && msg.baseV >= ownV) this.#ledger.set(key, msg.v);
7547
+ void Promise.resolve(this.#applyRemoteWrite(msg.vault, msg.collection, msg.docId, msg.action)).catch((err) => {
7548
+ console.warn(`[noy-db] cross-tab apply failed for ${msg.collection}/${msg.docId}: ` + (err instanceof Error ? err.message : String(err)));
7549
+ });
7550
+ }
7551
+ };
7552
+ function ledgerKey(vault, collection, docId) {
7553
+ return `${vault}\0${collection}\0${docId}`;
7554
+ }
7555
+ function isTabWriteMsg(x) {
7556
+ if (x === null || typeof x !== "object") return false;
7557
+ const o = x;
7558
+ 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";
7559
+ }
7560
+
6584
7561
  // src/session/unlock-state.ts
6585
7562
  var QuickUnlockStore = class {
6586
7563
  states = /* @__PURE__ */ new Map();
@@ -6629,6 +7606,9 @@ var NOT_ENABLED4 = new Error(
6629
7606
  var NO_TX = {
6630
7607
  async runTransaction() {
6631
7608
  throw NOT_ENABLED4;
7609
+ },
7610
+ async runDryRun() {
7611
+ throw NOT_ENABLED4;
6632
7612
  }
6633
7613
  };
6634
7614
 
@@ -6926,6 +7906,9 @@ function createPlaintextKeyring(userId) {
6926
7906
  var Noydb = class {
6927
7907
  options;
6928
7908
  emitter = new NoydbEventEmitter();
7909
+ writeQueueTracker = new WriteQueueTracker();
7910
+ writeHooks = new WriteHookRegistry();
7911
+ clientId = generateULID();
6929
7912
  vaultCache = /* @__PURE__ */ new Map();
6930
7913
  keyringCache = /* @__PURE__ */ new Map();
6931
7914
  syncEngines = /* @__PURE__ */ new Map();
@@ -6958,6 +7941,10 @@ var Noydb = class {
6958
7941
  publicEnvelopeSchema;
6959
7942
  closed = false;
6960
7943
  sessionTimer = null;
7944
+ /** Same-device multi-tab coordinator (#228); created on `enableTabCoordination()`. */
7945
+ tabCoordinator;
7946
+ /** Cross-tab write relay (#228b); created on `enableTabCoordination()`. */
7947
+ writeRelay;
6961
7948
  /** Per-vault policy enforcers. */
6962
7949
  policyEnforcers = /* @__PURE__ */ new Map();
6963
7950
  txStrategy;
@@ -7150,6 +8137,7 @@ var Noydb = class {
7150
8137
  await comp._initDerivations(this.options.derivationStrategies ?? []);
7151
8138
  await comp._initMaterializedViews(this.options.materializedViewStrategies ?? []);
7152
8139
  await comp._initOverlayedViews(this.options.overlayedViewStrategies ?? []);
8140
+ await comp.schemaFence.init();
7153
8141
  this.vaultCache.set(name, comp);
7154
8142
  return comp;
7155
8143
  }
@@ -7577,6 +8565,14 @@ var Noydb = class {
7577
8565
  if (typeof arg === "function") {
7578
8566
  return this.txStrategy.runTransaction(this, arg);
7579
8567
  }
8568
+ if (typeof arg === "object" && arg !== null && arg.dryRun === true) {
8569
+ if (typeof maybeFn !== "function") {
8570
+ throw new ValidationError(
8571
+ "db.transaction({ dryRun: true }, fn) requires the callback as the second argument."
8572
+ );
8573
+ }
8574
+ return this.txStrategy.runDryRun(this, maybeFn);
8575
+ }
7580
8576
  if (typeof arg === "object" && arg !== null && arg.amendment === true) {
7581
8577
  if (typeof maybeFn !== "function") {
7582
8578
  throw new ValidationError(
@@ -7689,6 +8685,133 @@ var Noydb = class {
7689
8685
  off(event, handler) {
7690
8686
  this.emitter.off(event, handler);
7691
8687
  }
8688
+ /**
8689
+ * Observable write-queue for this hub instance. Reflects outstanding
8690
+ * in-flight writes across all collections. See {@link WriteQueue}.
8691
+ *
8692
+ * @example
8693
+ * window.addEventListener('beforeunload', (e) => {
8694
+ * if (db.writeQueue.pending) { e.preventDefault(); e.returnValue = '' }
8695
+ * })
8696
+ */
8697
+ get writeQueue() {
8698
+ return this.writeQueueTracker;
8699
+ }
8700
+ /**
8701
+ * @internal Mutable tracker behind {@link writeQueue}. Threaded into
8702
+ * each Collection (via Vault) so `put`/`delete` can `track()` writes.
8703
+ * Not part of the public surface — consumers use `writeQueue`.
8704
+ */
8705
+ get _writeQueueTracker() {
8706
+ return this.writeQueueTracker;
8707
+ }
8708
+ /**
8709
+ * Register a hook that runs before each write (#230). Awaited; a throw
8710
+ * aborts the write. Returns an unsubscribe function.
8711
+ */
8712
+ onBeforeWrite(handler) {
8713
+ return this.writeHooks.onBeforeWrite(handler);
8714
+ }
8715
+ /**
8716
+ * Register a hook that runs after each committed write (#230). Awaited;
8717
+ * a handler error is warned, never rolled back. Returns an unsubscribe fn.
8718
+ */
8719
+ onAfterWrite(handler) {
8720
+ return this.writeHooks.onAfterWrite(handler);
8721
+ }
8722
+ /** Subscribe to cross-tab write conflicts (#228c). Returns an unsubscribe. */
8723
+ onWriteConflict(fn) {
8724
+ this.on("write:conflict", fn);
8725
+ return () => this.off("write:conflict", fn);
8726
+ }
8727
+ /**
8728
+ * Enable same-device multi-tab coordination (#228): primary/secondary
8729
+ * election + presence. Browser-only — a graceful no-op (role 'unknown')
8730
+ * when Web Locks / BroadcastChannel are unavailable and nothing is
8731
+ * injected. Idempotent; returns a disposer.
8732
+ */
8733
+ enableTabCoordination(opts = {}) {
8734
+ if (this.tabCoordinator) return { dispose: () => this.disableTabCoordination() };
8735
+ const lockManager = opts.lockManager ?? defaultLockManager();
8736
+ const channel = opts.channel ?? defaultChannel();
8737
+ const c = new TabCoordinator({
8738
+ ...opts,
8739
+ ...lockManager ? { lockManager } : {},
8740
+ ...channel ? { channel } : {},
8741
+ // We own the channel only when we created the default; never close a caller-injected one.
8742
+ closeChannelOnDispose: opts.channel === void 0 && channel !== void 0
8743
+ });
8744
+ this.tabCoordinator = c;
8745
+ c.start();
8746
+ if (opts.propagateWrites !== false) {
8747
+ const writeChannel = opts.writeChannel ?? defaultChannel("noydb:tab-writes");
8748
+ if (writeChannel) {
8749
+ const relay = new CrossTabWriteRelay({
8750
+ channel: writeChannel,
8751
+ writerId: c.tabId,
8752
+ subscribeAfterWrite: (h) => this.onAfterWrite(h),
8753
+ applyRemoteWrite: (vault, collection, docId, action) => this.#applyRemoteWrite(vault, collection, docId, action),
8754
+ reportConflict: (vault, collection, docId, action, baseV, v, ownV) => this.#reportWriteConflict(vault, collection, docId, action, baseV, v, ownV),
8755
+ // Own the channel only when we created the default (mirrors the presence channel).
8756
+ closeChannelOnDispose: opts.writeChannel === void 0 && writeChannel !== void 0
8757
+ });
8758
+ this.writeRelay = relay;
8759
+ relay.start();
8760
+ }
8761
+ }
8762
+ return { dispose: () => this.disableTabCoordination() };
8763
+ }
8764
+ #applyRemoteWrite(vaultName, collectionName, docId, action) {
8765
+ const v = this.vaultCache.get(vaultName);
8766
+ if (!v) return Promise.resolve();
8767
+ return v._applyRemoteWrite(collectionName, docId, action);
8768
+ }
8769
+ async #reportWriteConflict(vaultName, collectionName, docId, action, baseV, v, ownV) {
8770
+ const vault = this.vaultCache.get(vaultName);
8771
+ if (!vault) return;
8772
+ const cap = await vault._captureAndConverge(collectionName, docId, action, baseV);
8773
+ if (!cap) return;
8774
+ const conflict = {
8775
+ vault: vaultName,
8776
+ collection: collectionName,
8777
+ docId,
8778
+ local: cap.local,
8779
+ remote: cap.remote,
8780
+ base: cap.base,
8781
+ localVersion: ownV,
8782
+ remoteVersion: v,
8783
+ baseVersion: baseV
8784
+ };
8785
+ this.emitter.emit("write:conflict", conflict);
8786
+ }
8787
+ disableTabCoordination() {
8788
+ this.tabCoordinator?.dispose();
8789
+ this.tabCoordinator = void 0;
8790
+ this.writeRelay?.dispose();
8791
+ this.writeRelay = void 0;
8792
+ }
8793
+ get tabRole() {
8794
+ return this.tabCoordinator?.role ?? "unknown";
8795
+ }
8796
+ activeTabs() {
8797
+ return this.tabCoordinator?.activeTabs() ?? [];
8798
+ }
8799
+ onTabRoleChange(fn) {
8800
+ return this.tabCoordinator?.onTabRoleChange(fn) ?? (() => {
8801
+ });
8802
+ }
8803
+ onActiveTabsChange(fn) {
8804
+ return this.tabCoordinator?.onActiveTabsChange(fn) ?? (() => {
8805
+ });
8806
+ }
8807
+ /** @internal The write-hook registry, threaded into each Collection. */
8808
+ get _writeHooks() {
8809
+ return this.writeHooks;
8810
+ }
8811
+ /** @internal Stable per-instance id for schema-cutover coordination (#232). */
8812
+ get _clientId() {
8813
+ return this.clientId;
8814
+ }
7692
8815
  /**
7693
8816
  * Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
7694
8817
  * instance, sync engine, policy enforcer, and active-tier entry —
@@ -7715,6 +8838,7 @@ var Noydb = class {
7715
8838
  this.syncEngines.delete(vault);
7716
8839
  this.policyEnforcers.get(vault)?.destroy();
7717
8840
  this.policyEnforcers.delete(vault);
8841
+ this.vaultCache.get(vault)?._stopFenceCoordination();
7718
8842
  this.keyringCache.delete(vault);
7719
8843
  this.vaultCache.delete(vault);
7720
8844
  this.activeTier.delete(vault);
@@ -7734,6 +8858,8 @@ var Noydb = class {
7734
8858
  engine.stopAutoSync();
7735
8859
  }
7736
8860
  this.syncEngines.clear();
8861
+ for (const v of this.vaultCache.values()) v._stopFenceCoordination();
8862
+ this.disableTabCoordination();
7737
8863
  this.keyringCache.clear();
7738
8864
  this.vaultCache.clear();
7739
8865
  this.activeTier.clear();
@@ -8831,4 +9957,4 @@ export {
8831
9957
  Noydb,
8832
9958
  createNoydb
8833
9959
  };
8834
- //# sourceMappingURL=chunk-E535SAN4.js.map
9960
+ //# sourceMappingURL=chunk-T6HQMVML.js.map