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