@noy-db/hub 0.7.0-pre.13 → 0.7.0-pre.15
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/CHANGELOG.md +169 -0
- package/README.md +4 -3
- package/dist/{api-UE2XQKDN.js → api-JPZT6BDD.js} +1 -2
- package/dist/as/index.js +3 -4
- package/dist/as/index.js.map +1 -1
- package/dist/at/index.js +0 -1
- package/dist/attestation/index.js +6 -7
- package/dist/attestation/index.js.map +1 -1
- package/dist/{backup-R6A4VZRX.js → backup-AKWIMYWU.js} +1 -2
- package/dist/{backup-R6A4VZRX.js.map → backup-AKWIMYWU.js.map} +1 -1
- package/dist/blobs/index.js +0 -1
- package/dist/blobs/index.js.map +1 -1
- package/dist/broker/index.js +3 -4
- package/dist/broker/index.js.map +1 -1
- package/dist/by/index.js +0 -1
- package/dist/cargo/index.js +10 -11
- package/dist/{chunk-5FUULQ6F.js → chunk-2HVILGVR.js} +6 -6
- package/dist/{chunk-DJ3LNNZQ.js → chunk-3LK2XZCW.js} +2 -2
- package/dist/{chunk-PQKG5AHH.js → chunk-6X6SEMEE.js} +4 -2
- package/dist/{chunk-PQKG5AHH.js.map → chunk-6X6SEMEE.js.map} +1 -1
- package/dist/{chunk-E2MYJGSG.js → chunk-AMODH7AU.js} +2 -2
- package/dist/{chunk-B2ZSGYSF.js → chunk-B2KCRDJM.js} +3 -3
- package/dist/{chunk-TO3EQ7HF.js → chunk-BLN54W3L.js} +2 -2
- package/dist/{chunk-IHFXR2VQ.js → chunk-BOBFL5TW.js} +2 -2
- package/dist/{chunk-ZR2YMODS.js → chunk-EKZ6VZYF.js} +2 -2
- package/dist/{chunk-RTYJRFQO.js → chunk-EWPRPP3M.js} +1 -1
- package/dist/{chunk-VGICZPVE.js → chunk-FUJSIP4K.js} +2 -2
- package/dist/{chunk-BWXGSSFX.js → chunk-NJXX55QX.js} +79 -16
- package/dist/chunk-NJXX55QX.js.map +1 -0
- package/dist/{chunk-PKJ43CMD.js → chunk-NLOLF6LD.js} +2 -2
- package/dist/{chunk-COTPDJR5.js → chunk-OMB2ZKIT.js} +8 -8
- package/dist/{chunk-BMELYFHU.js → chunk-P3KOF624.js} +3 -3
- package/dist/{chunk-Y4MJQ2UG.js → chunk-QQ2YMQZU.js} +2 -2
- package/dist/{chunk-URHRIAV6.js → chunk-REDOOY2Q.js} +2 -2
- package/dist/chunk-REDOOY2Q.js.map +1 -0
- package/dist/{chunk-YCXICMOY.js → chunk-RJ7NN2UR.js} +2 -2
- package/dist/{chunk-PDD75NUZ.js → chunk-SMH6SRX7.js} +2 -2
- package/dist/{chunk-QHW35ZON.js → chunk-TGPST6FK.js} +2 -2
- package/dist/{chunk-BERJ4AWW.js → chunk-VGXZT72I.js} +2 -2
- package/dist/{chunk-ZVCC36XQ.js → chunk-WL36WA3O.js} +55 -55
- package/dist/{chunk-2Z47NMFK.js → chunk-WZW37ZHL.js} +2 -2
- package/dist/{chunk-SXCYYMVS.js → chunk-YINEDONN.js} +5 -5
- package/dist/{chunk-AJQY44CE.js → chunk-ZXLZ62R5.js} +3 -20
- package/dist/chunk-ZXLZ62R5.js.map +1 -0
- package/dist/classified/index.js +1 -2
- package/dist/{classified-marker-67HBIJMZ.js → classified-marker-OO65UYXN.js} +3 -5
- package/dist/{classified-marker-67HBIJMZ.js.map → classified-marker-OO65UYXN.js.map} +1 -1
- package/dist/{collection-facade-4ACKIDDY.js → collection-facade-VOWW732S.js} +3 -4
- package/dist/{computed-LSEZYRJS.js → computed-VPOKQK4N.js} +1 -2
- package/dist/consent/index.js +0 -1
- package/dist/consent/index.js.map +1 -1
- package/dist/cover/index.js +0 -1
- package/dist/crdt/index.js +0 -1
- package/dist/crdt/index.js.map +1 -1
- package/dist/custody/index.js +1 -2
- package/dist/{dead-filter-M3NZUWBZ.js → dead-filter-OMMVBAKZ.js} +2 -3
- package/dist/{dead-filter-M3NZUWBZ.js.map → dead-filter-OMMVBAKZ.js.map} +1 -1
- package/dist/debug/index.js +0 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/{delegation-OLJUWTVA.js → delegation-VRYMQRCF.js} +1 -2
- package/dist/{delegation-OLJUWTVA.js.map → delegation-VRYMQRCF.js.map} +1 -1
- package/dist/derivations/index.js +3 -4
- package/dist/{derive-LY47HG7Q.js → derive-HHBRFS2U.js} +2 -3
- package/dist/directory/index.js +0 -1
- package/dist/{dispatch-MSTR5VBI.js → dispatch-DC3LQJ3S.js} +6 -7
- package/dist/{dispatch-MSTR5VBI.js.map → dispatch-DC3LQJ3S.js.map} +1 -1
- package/dist/{dispatch-PDP5TA73.js → dispatch-GGFBWQU7.js} +5 -7
- package/dist/{dispatch-PDP5TA73.js.map → dispatch-GGFBWQU7.js.map} +1 -1
- package/dist/{enclave-4YBQK77G.js → enclave-VM2YQONI.js} +1 -2
- package/dist/{executor-TOLPBHOC.js → executor-5NIMLSKZ.js} +1 -2
- package/dist/{executor-YDDC5XGY.js → executor-5XJNV5H5.js} +1 -2
- package/dist/{executor-MZJXBWNM.js → executor-AQVXV5GZ.js} +1 -2
- package/dist/{export-accessible-LGV6IAFX.js → export-accessible-2LOBM67T.js} +3 -4
- package/dist/{extract-partition-RYJZIKQZ.js → extract-partition-AZGSEMIF.js} +3 -4
- package/dist/{fanout-sidecar-TSFPXY5C.js → fanout-sidecar-HJIVUPTD.js} +1 -2
- package/dist/{fanout-sidecar-TSFPXY5C.js.map → fanout-sidecar-HJIVUPTD.js.map} +1 -1
- package/dist/{fence-watcher-ZVSMLKQD.js → fence-watcher-U2PW6JU5.js} +1 -3
- package/dist/{fence-watcher-ZVSMLKQD.js.map → fence-watcher-U2PW6JU5.js.map} +1 -1
- package/dist/{find-LC7P63MX.js → find-2ABFCZX4.js} +1 -2
- package/dist/forget/index.js +0 -1
- package/dist/forget/index.js.map +1 -1
- package/dist/guards/index.js +0 -1
- package/dist/history/index.js +0 -1
- package/dist/history/index.js.map +1 -1
- package/dist/i18n/index.js +0 -1
- package/dist/i18n/index.js.map +1 -1
- package/dist/index.js +26 -27
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.js +0 -1
- package/dist/indexing/index.js.map +1 -1
- package/dist/introspection/index.js +2 -3
- package/dist/{issue-IOXM3RSJ.js → issue-RH4H733Z.js} +1 -2
- package/dist/{json-schema-I22JCYCG.js → json-schema-V3M5MUCM.js} +1 -3
- package/dist/{json-schema-I22JCYCG.js.map → json-schema-V3M5MUCM.js.map} +1 -1
- package/dist/kernel/collection-config.d.ts +34 -0
- package/dist/lazy/index.js +0 -1
- package/dist/{ledger-HXHIOD36.js → ledger-454SU3VE.js} +1 -2
- package/dist/{liberate-4APIV7FR.js → liberate-AAZSP2BT.js} +5 -6
- package/dist/{link-set-6RAHEKZN.js → link-set-67CAMUAL.js} +1 -2
- package/dist/{managed-secret-P3JQ7ZG4.js → managed-secret-NHRYG7CD.js} +1 -2
- package/dist/materialized-views/index.js +2 -3
- package/dist/{migrate-cek-25KNTS5U.js → migrate-cek-5ENH64AA.js} +1 -2
- package/dist/{migrate-cek-25KNTS5U.js.map → migrate-cek-5ENH64AA.js.map} +1 -1
- package/dist/money/index.js +0 -1
- package/dist/{noydb-2C6QQJ5S.js → noydb-F5CABCU4.js} +11 -12
- package/dist/overlay-views/index.js +0 -1
- package/dist/periods/index.js +1 -2
- package/dist/pod/index.js +12 -13
- package/dist/{pod-handle-6II7KFBR.js → pod-handle-S7AMCJIF.js} +2 -3
- package/dist/{pod-handle-6II7KFBR.js.map → pod-handle-S7AMCJIF.js.map} +1 -1
- package/dist/policy/index.js +0 -1
- package/dist/portability/index.js +6 -7
- package/dist/portability/index.js.map +1 -1
- package/dist/{post-register-BPJ36A6D.js → post-register-HIYY4Y5S.js} +4 -5
- package/dist/{post-register-BPJ36A6D.js.map → post-register-HIYY4Y5S.js.map} +1 -1
- package/dist/{purge-scope-FGNNLWHD.js → purge-scope-4HEFPDOW.js} +1 -3
- package/dist/{purge-scope-FGNNLWHD.js.map → purge-scope-4HEFPDOW.js.map} +1 -1
- package/dist/query/index.js +0 -1
- package/dist/{read-only-facade-CHCKSEPD.js → read-only-facade-BIGO37U3.js} +1 -2
- package/dist/reduce/index.js +0 -1
- package/dist/reduce/index.js.map +1 -1
- package/dist/{register-AEOOGKNO.js → register-7OJWAWT5.js} +3 -4
- package/dist/{registry-SKSOUXNF.js → registry-23WF3TGH.js} +1 -2
- package/dist/{registry-SHMGHEK4.js → registry-4AYVUNXK.js} +1 -2
- package/dist/{registry-I6FP4677.js → registry-5MVJGPXW.js} +1 -2
- package/dist/{registry-D2YXKRYC.js → registry-BOHIKFWG.js} +4 -5
- package/dist/{request-withdrawal-6HJT4ODF.js → request-withdrawal-P5EQATZ7.js} +5 -6
- package/dist/{reveal-DDQBDWPM.js → reveal-IMGQ2EE5.js} +1 -2
- package/dist/{reveal-DDQBDWPM.js.map → reveal-IMGQ2EE5.js.map} +1 -1
- package/dist/{revoke-U7U3FMXU.js → revoke-POHP4GPB.js} +1 -2
- package/dist/satellites/index.js +0 -1
- package/dist/schema-update/index.js +0 -1
- package/dist/sealed-record/index.js +3 -4
- package/dist/sealed-record/index.js.map +1 -1
- package/dist/search/index.js +1 -2
- package/dist/{seed-3RXT4VGN.js → seed-UYR6YG3V.js} +1 -2
- package/dist/{seed-3RXT4VGN.js.map → seed-UYR6YG3V.js.map} +1 -1
- package/dist/sequence/index.js +0 -1
- package/dist/session/index.js +0 -1
- package/dist/session/index.js.map +1 -1
- package/dist/shadow/index.js +0 -1
- package/dist/shadow/index.js.map +1 -1
- package/dist/share-link/index.js +0 -1
- package/dist/share-link/index.js.map +1 -1
- package/dist/{signer-7NK7TMWU.js → signer-5K3WDHOC.js} +1 -2
- package/dist/snapshots/index.js +1 -2
- package/dist/snapshots/index.js.map +1 -1
- package/dist/{stale-2CHKZTMK.js → stale-AU74OPUW.js} +2 -3
- package/dist/{storage-WP4M7IFO.js → storage-AVJA5BTW.js} +1 -2
- package/dist/store/index.js +0 -1
- package/dist/{store-coordination-provider-O2VUEMJH.js → store-coordination-provider-RX2CGWXV.js} +1 -2
- package/dist/{store-coordination-provider-O2VUEMJH.js.map → store-coordination-provider-RX2CGWXV.js.map} +1 -1
- package/dist/sync/index.js +0 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/team/index.js +0 -1
- package/dist/tiers/index.js +1 -2
- package/dist/to/index.js +0 -1
- package/dist/to/index.js.map +1 -1
- package/dist/transactions/index.js +2 -3
- package/dist/transactions/index.js.map +1 -1
- package/dist/{ulid-VQW6ERQN.js → ulid-4MKXG325.js} +1 -2
- package/dist/util/index.js +0 -1
- package/dist/util/index.js.map +1 -1
- package/dist/{vault-diff-FE5J35NJ.js → vault-diff-C6WUJFNP.js} +1 -2
- package/dist/vault-head/index.js +0 -1
- package/dist/vault-head/index.js.map +1 -1
- package/dist/{verify-JBXCAGDL.js → verify-KOV2G5D6.js} +1 -2
- package/dist/{verify-JBXCAGDL.js.map → verify-KOV2G5D6.js.map} +1 -1
- package/dist/{walk-P3E6QSNC.js → walk-ERAJ5RHO.js} +3 -4
- package/dist/with-formula/derivations/registry.d.ts +6 -0
- package/dist/with-shape/introspection/describe.d.ts +2 -53
- package/dist/with-shape/introspection/field-meta.d.ts +86 -0
- package/dist/{withdraw-accessible-6Q7NE4EY.js → withdraw-accessible-42D32Z7G.js} +4 -5
- package/package.json +7 -3
- package/dist/chunk-AJQY44CE.js.map +0 -1
- package/dist/chunk-BWXGSSFX.js.map +0 -1
- package/dist/chunk-PZ5AY32C.js +0 -10
- package/dist/chunk-URHRIAV6.js.map +0 -1
- package/dist/withdraw-accessible-6Q7NE4EY.js.map +0 -1
- package/dist/zod-HAJM7LMS.js +0 -14763
- package/dist/zod-HAJM7LMS.js.map +0 -1
- /package/dist/{api-UE2XQKDN.js.map → api-JPZT6BDD.js.map} +0 -0
- /package/dist/{chunk-5FUULQ6F.js.map → chunk-2HVILGVR.js.map} +0 -0
- /package/dist/{chunk-DJ3LNNZQ.js.map → chunk-3LK2XZCW.js.map} +0 -0
- /package/dist/{chunk-E2MYJGSG.js.map → chunk-AMODH7AU.js.map} +0 -0
- /package/dist/{chunk-B2ZSGYSF.js.map → chunk-B2KCRDJM.js.map} +0 -0
- /package/dist/{chunk-TO3EQ7HF.js.map → chunk-BLN54W3L.js.map} +0 -0
- /package/dist/{chunk-IHFXR2VQ.js.map → chunk-BOBFL5TW.js.map} +0 -0
- /package/dist/{chunk-ZR2YMODS.js.map → chunk-EKZ6VZYF.js.map} +0 -0
- /package/dist/{chunk-RTYJRFQO.js.map → chunk-EWPRPP3M.js.map} +0 -0
- /package/dist/{chunk-VGICZPVE.js.map → chunk-FUJSIP4K.js.map} +0 -0
- /package/dist/{chunk-PKJ43CMD.js.map → chunk-NLOLF6LD.js.map} +0 -0
- /package/dist/{chunk-COTPDJR5.js.map → chunk-OMB2ZKIT.js.map} +0 -0
- /package/dist/{chunk-BMELYFHU.js.map → chunk-P3KOF624.js.map} +0 -0
- /package/dist/{chunk-Y4MJQ2UG.js.map → chunk-QQ2YMQZU.js.map} +0 -0
- /package/dist/{chunk-YCXICMOY.js.map → chunk-RJ7NN2UR.js.map} +0 -0
- /package/dist/{chunk-PDD75NUZ.js.map → chunk-SMH6SRX7.js.map} +0 -0
- /package/dist/{chunk-QHW35ZON.js.map → chunk-TGPST6FK.js.map} +0 -0
- /package/dist/{chunk-BERJ4AWW.js.map → chunk-VGXZT72I.js.map} +0 -0
- /package/dist/{chunk-ZVCC36XQ.js.map → chunk-WL36WA3O.js.map} +0 -0
- /package/dist/{chunk-2Z47NMFK.js.map → chunk-WZW37ZHL.js.map} +0 -0
- /package/dist/{chunk-SXCYYMVS.js.map → chunk-YINEDONN.js.map} +0 -0
- /package/dist/{chunk-PZ5AY32C.js.map → collection-facade-VOWW732S.js.map} +0 -0
- /package/dist/{collection-facade-4ACKIDDY.js.map → computed-VPOKQK4N.js.map} +0 -0
- /package/dist/{computed-LSEZYRJS.js.map → derive-HHBRFS2U.js.map} +0 -0
- /package/dist/{derive-LY47HG7Q.js.map → enclave-VM2YQONI.js.map} +0 -0
- /package/dist/{enclave-4YBQK77G.js.map → executor-5NIMLSKZ.js.map} +0 -0
- /package/dist/{executor-MZJXBWNM.js.map → executor-5XJNV5H5.js.map} +0 -0
- /package/dist/{executor-TOLPBHOC.js.map → executor-AQVXV5GZ.js.map} +0 -0
- /package/dist/{executor-YDDC5XGY.js.map → export-accessible-2LOBM67T.js.map} +0 -0
- /package/dist/{export-accessible-LGV6IAFX.js.map → extract-partition-AZGSEMIF.js.map} +0 -0
- /package/dist/{extract-partition-RYJZIKQZ.js.map → find-2ABFCZX4.js.map} +0 -0
- /package/dist/{find-LC7P63MX.js.map → issue-RH4H733Z.js.map} +0 -0
- /package/dist/{issue-IOXM3RSJ.js.map → ledger-454SU3VE.js.map} +0 -0
- /package/dist/{ledger-HXHIOD36.js.map → liberate-AAZSP2BT.js.map} +0 -0
- /package/dist/{liberate-4APIV7FR.js.map → link-set-67CAMUAL.js.map} +0 -0
- /package/dist/{link-set-6RAHEKZN.js.map → managed-secret-NHRYG7CD.js.map} +0 -0
- /package/dist/{managed-secret-P3JQ7ZG4.js.map → noydb-F5CABCU4.js.map} +0 -0
- /package/dist/{noydb-2C6QQJ5S.js.map → read-only-facade-BIGO37U3.js.map} +0 -0
- /package/dist/{read-only-facade-CHCKSEPD.js.map → register-7OJWAWT5.js.map} +0 -0
- /package/dist/{register-AEOOGKNO.js.map → registry-23WF3TGH.js.map} +0 -0
- /package/dist/{registry-D2YXKRYC.js.map → registry-4AYVUNXK.js.map} +0 -0
- /package/dist/{registry-I6FP4677.js.map → registry-5MVJGPXW.js.map} +0 -0
- /package/dist/{registry-SHMGHEK4.js.map → registry-BOHIKFWG.js.map} +0 -0
- /package/dist/{registry-SKSOUXNF.js.map → request-withdrawal-P5EQATZ7.js.map} +0 -0
- /package/dist/{request-withdrawal-6HJT4ODF.js.map → revoke-POHP4GPB.js.map} +0 -0
- /package/dist/{revoke-U7U3FMXU.js.map → signer-5K3WDHOC.js.map} +0 -0
- /package/dist/{signer-7NK7TMWU.js.map → stale-AU74OPUW.js.map} +0 -0
- /package/dist/{stale-2CHKZTMK.js.map → storage-AVJA5BTW.js.map} +0 -0
- /package/dist/{storage-WP4M7IFO.js.map → ulid-4MKXG325.js.map} +0 -0
- /package/dist/{ulid-VQW6ERQN.js.map → vault-diff-C6WUJFNP.js.map} +0 -0
- /package/dist/{vault-diff-FE5J35NJ.js.map → walk-ERAJ5RHO.js.map} +0 -0
- /package/dist/{walk-P3E6QSNC.js.map → withdraw-accessible-42D32Z7G.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-formula/derivations/dispatch.ts"],"sourcesContent":["/**\n * Derivation dispatch, lifted out of the kernel spine (#842 part b).\n *\n * `Collection` carried ~380 lines of dispatch logic that belongs to the\n * derivation service, not to the always-on kernel. These are free functions\n * taking an explicit context so the spine keeps only a thin delegator.\n *\n * ## How the spine reaches this module\n *\n * By dynamic `import()`, never a static one. `check-architecture.mjs`'s\n * `port-layering` check forbids the spine from statically importing a\n * `with-*` service; `collection.ts` is grandfathered for ten\n * `with-formula/*` specifiers, but that list is frozen per file and a new\n * one is a violation. A dynamic import is the sanctioned S4 gate recipe, and\n * it keeps this chunk out of the floor bundle for consumers who never fire a\n * derivation.\n *\n * The `Collection` type reference below points back at the spine. That is\n * inward (service → kernel) and so allowed, and it is TYPE-ONLY — erased at\n * build, so no runtime cycle exists.\n *\n * @internal\n */\n\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { TxContext } from '../../with-commit/tx/transaction.js'\nimport type { DerivationRegistry } from './registry.js'\nimport type { ReadOnlyVaultFacade } from '../../with-audit/guards/types.js'\nimport type { Collection } from '../../kernel/collection.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { EncryptedEnvelope } from '../../kernel/types.js'\nimport type { ledgerAuditHook, WaveContext } from '../../kernel/via/dispatch.js'\nimport { putDerivedOutput, selfWriteFieldEqual } from '../../kernel/via/dispatch.js'\nimport { DerivationCapExceededError } from '../../kernel/errors.js'\nimport { tupleFromWritten, sameTuple } from './trigger-match.js'\nimport { markStale } from './stale.js'\nimport type { DerivationExecutor } from './executor.js'\n\n/** The per-write dispatch context threaded into `putDerivedOutput`. */\ninterface DispatchCtx {\n emit: (e: string, p: unknown) => void\n source: { readonly collection: string; readonly id: string }\n audit: ReturnType<typeof ledgerAuditHook>\n}\n\n/**\n * What the dispatchers need from the collection that owns them. Passing this\n * explicitly is what lets the logic live outside the class.\n */\nexport interface DerivationDeleteCtx {\n readonly derivationSource: {\n registry(): DerivationRegistry\n getCollection(name: string): Collection<Record<string, unknown>>\n getActiveTxContext(): TxContext | null\n getReadOnlyFacade(): ReadOnlyVaultFacade\n }\n /** The collection the delete happened in. */\n readonly collectionName: string\n readonly adapter: NoydbStore\n readonly vault: string\n readonly getDEK: (collectionName: string) => Promise<EnclaveKey>\n readonly storeCiphertext: boolean\n}\n\n/**\n * Erase the derived rows a deleted source record fanned out to, and drop the\n * fan-out sidecars that recorded them.\n *\n * @param eraseRecordShapeToo also erase same-id `shape: 'record'` outputs.\n * Restricted to a source-triggered strategy writing into a DIFFERENT\n * collection — the self-denorm case would re-delete the record just\n * tombstoned, and triggerBy/sibling strategies derive under a different id.\n * @returns rows ACTUALLY deleted, not edges visited (#622).\n */\nexport async function dispatchArrayDerivationsOnDelete(\n ctx: DerivationDeleteCtx,\n id: string,\n eraseRecordShapeToo = false,\n): Promise<number> {\n const { derivationSource, collectionName, adapter, vault, getDEK, storeCiphertext } = ctx\n\n const strategies = derivationSource.registry().strategiesForSource(collectionName)\n if (strategies.length === 0) return 0\n\n const { loadFanoutSidecar, deleteFanoutSidecar } = await import('./fanout-sidecar.js')\n const txCtx = derivationSource.getActiveTxContext()\n let erased = 0\n\n for (const { spec } of strategies) {\n for (const [outputKey, outSpec] of Object.entries(spec.outputs)) {\n if (outSpec.shape === 'record') {\n if (eraseRecordShapeToo && spec.source === collectionName && outSpec.collection !== collectionName) {\n if (await derivationSource.getCollection(outSpec.collection)._internalDelete(id, txCtx)) erased += 1\n }\n continue\n }\n\n const sidecar = await loadFanoutSidecar(adapter, vault, spec.source, id, outputKey, getDEK, storeCiphertext)\n if (!sidecar) continue\n\n const outputCollection = derivationSource.getCollection(outSpec.collection)\n for (const derivedId of sidecar.keys) {\n if (await outputCollection._internalDelete(derivedId, txCtx)) erased += 1\n }\n await deleteFanoutSidecar(adapter, vault, spec.source, id, outputKey)\n }\n }\n\n return erased\n}\n\n/** Adds what the on-write derivation dispatch needs beyond the delete path. */\nexport interface DerivationDispatchCtx extends DerivationDeleteCtx {\n /** Via pipeline, for decoding the stored form back to canonical money shape. */\n readonly via: { canonicalizeStored(r: Record<string, unknown>): Record<string, unknown> } | undefined\n /** Recompute a rollup aggregate onto a parent. Stays on the spine — it walks collections. */\n readonly recomputeRollup: (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n spec: any,\n parentId: string,\n source: { readonly collection: string; readonly id: string },\n wave?: WaveContext,\n ) => Promise<unknown>\n /** Builds the per-write dispatch context threaded into `putDerivedOutput`. */\n readonly dispatchCtx: (source: { readonly collection: string; readonly id: string }) => DispatchCtx\n /** Registers a write on the active transaction for rollback. */\n readonly trackPut: (txCtx: TxContext, collectionName: string, id: string, priorEnvelope: EncryptedEnvelope | null) => void\n}\n\n/**\n * Fire registered derivation strategies for this source collection.\n *\n * Eager mode runs `derive` inline and writes each output; lazy mode records a\n * stale mark for cold-session recompute. Rollups are handled here rather than\n * by the executor — a write to the child recomputes the parent aggregate, and\n * a write to the parent recomputes its own.\n */\nexport async function dispatchDerivations(\n ctx: DerivationDispatchCtx,\n id: string,\n record: Record<string, unknown>,\n version: number,\n wave?: WaveContext,\n /** The pre-write record at `id` in THIS collection (undefined = not\n * captured — no field-match trigger, or a sync-applied wave; null =\n * captured and absent, i.e. a create). Only a `Record` re-fires the OLD\n * matched set alongside the new one (spec §7). */\n prior?: Record<string, unknown> | null,\n): Promise<void> {\n const { derivationSource, collectionName, via, recomputeRollup } = ctx\n\n // `record` is the stored form here (post-quantize) — decode so\n // derive(source, ctx) sees the canonical money shape.\n const incoming = (via ? via.canonicalizeStored(record) : record)\n if (incoming && typeof incoming === 'object' && '_derivedFrom' in incoming) return\n // `prior` is the raw stored pre-write record — canonicalize it the same\n // way as `incoming` before it feeds `tupleFromWritten` below, or a\n // via-shaped match field (e.g. a money field) compares canonical against\n // raw and never matches the old tuple (Min 5).\n const canonicalPrior = prior != null ? (via ? via.canonicalizeStored(prior) : prior) : prior\n const registry = derivationSource.registry()\n const strategies = registry.strategiesForSource(collectionName)\n if (strategies.length === 0) return\n // Dynamic-import the executor only on the first eager-mode\n // dispatch. Lazy-mode dispatches use `markStale` (a pure helper)\n // which doesn't reach into the executor at all. Keeps the\n // derivation executor chunk out of the floor bundle for any\n // consumer that doesn't fire an eager derivation.\n let executorClass: typeof DerivationExecutor | null = null\n for (const { spec, strategyHash, triggers } of strategies) {\n const mode = typeof spec.lifecycle === 'string' ? spec.lifecycle : spec.lifecycle.mode\n\n // Rollup: a write to the child `from` recomputes the\n // parent at id child[key]; a write to the parent (source = into)\n // recomputes its own aggregate. Handled here (the executor is not run).\n if (spec.rollup) {\n if (mode !== 'eager') continue\n const asParentId = (v: unknown): string | null =>\n (typeof v === 'string' || typeof v === 'number') ? String(v) : null\n let parentId: string | null\n // #1257 — a child that RE-PARENTS strands the old parent's aggregate\n // unless the old key is recomputed too. Reads the prior stored record\n // (canonicalized alongside `incoming`, so a via-shaped key compares like\n // for like). `undefined` prior — a sync-applied wave write or a tiers\n // restore, which do not thread it — degrades to new-parent-only, the\n // pre-#1257 behaviour, rather than guessing.\n let oldParentId: string | null = null\n if (collectionName === spec.rollup.from) {\n parentId = asParentId(incoming[spec.rollup.key])\n const priorKey = canonicalPrior != null ? asParentId(canonicalPrior[spec.rollup.key]) : null\n if (priorKey !== null && priorKey !== parentId) oldParentId = priorKey\n } else {\n parentId = id // a write to the parent recomputes its own aggregate\n }\n if (parentId !== null) await recomputeRollup(spec, parentId, { collection: collectionName, id }, wave)\n // The old parent is recomputed AFTER the new one so a `maxFanout`-style\n // failure on the new parent surfaces before extra work is spent.\n if (oldParentId !== null) await recomputeRollup(spec, oldParentId, { collection: collectionName, id }, wave)\n continue\n }\n\n // Determine how `collectionName` triggers this strategy, and build the list\n // of source records to (re-)derive:\n // • source — re-derive the written record itself (same-id).\n // • sources[] — re-derive the PRIMARY source at the same id.\n // • triggerBy — FK fan-out: re-derive every source record\n // whose `on` field equals the written parent's id.\n // `input` is passed to derive(); `base` is the raw stored source record\n // used as the patch base for a self-write reverse-denorm output.\n const isSource = spec.source === collectionName\n const isSibling = !isSource && (spec.sources?.includes(collectionName) ?? false)\n const triggerEntries = !isSource && !isSibling\n ? triggers.filter((t) => t.collection === collectionName)\n : []\n\n const runs: Array<{\n input: Record<string, unknown> & { id: string }\n base: Record<string, unknown>\n runId: string\n version: number\n }> = []\n\n if (isSource) {\n runs.push({ input: { ...incoming, id }, base: incoming, runId: id, version })\n } else if (isSibling) {\n const p = await derivationSource.getCollection(spec.source).get(id)\n if (p !== null && p !== undefined) {\n // Raw base for a (rare) sibling self-write; falls back to the\n // resolved primary if the raw read misses.\n const raw = await derivationSource.getCollection(spec.source)._getStoredRecord(id)\n runs.push({ input: { ...p, id }, base: raw ?? p, runId: id, version: 0 })\n }\n } else if (triggerEntries.length > 0) {\n const srcColl = derivationSource.getCollection(spec.source)\n const matched = new Set<string>()\n for (const trigger of triggerEntries) {\n // Union fan-out (spec §7): an update changing any matched component\n // must re-fire BOTH the old and the new matched set, or the old\n // set's derived output goes stale (never re-derived once it no\n // longer matches). `prior` is only ever a `Record` here — a wave\n // dispatch and a create both skip the old tuple outright.\n const tuples = [tupleFromWritten(trigger.match, id, incoming)]\n if (canonicalPrior != null && trigger.match.some((p) => p.from !== 'id')) {\n const old = tupleFromWritten(trigger.match, id, canonicalPrior)\n if (!sameTuple(old, tuples[0]!)) tuples.push(old)\n }\n const ids = new Set<string>()\n for (const tuple of tuples) {\n if (tuple === null) continue // a from-value is absent/non-scalar: matches nothing\n for (const sid of await srcColl._findMatchingCompositeIds(tuple)) ids.add(sid)\n }\n if (trigger.maxFanout !== undefined && ids.size > trigger.maxFanout) {\n throw new DerivationCapExceededError(\n `triggerBy ${collectionName}→${spec.source} [${trigger.match.map(p => p.to).join(',')}]`,\n ids.size, trigger.maxFanout)\n }\n for (const sid of ids) matched.add(sid)\n }\n for (const sid of matched) {\n const raw = await srcColl._getStoredRecord(sid)\n if (raw === null) continue\n runs.push({ input: { ...raw, id: sid }, base: raw, runId: sid, version: 0 })\n }\n }\n\n if (runs.length === 0) continue\n\n if (mode !== 'eager') {\n for (const run of runs) await markStale(registry, spec, run.runId)\n continue\n }\n\n if (executorClass === null) {\n ;({ DerivationExecutor: executorClass } = await import('./executor.js'))\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n for (const run of runs) await runOne(ctx, spec as any, strategyHash, executorClass, run)\n }\n}\n\n/**\n * Run the executor for one (spec, run) pair and write its outputs. Shared by\n * `dispatchDerivations`' eager loop and `dispatchTriggerDerivationsOnDelete`\n * (#1249) — the write-out logic (array diff, record self-write reverse-denorm,\n * normal record output) is identical for a live write and a delete-triggered\n * re-derive; only how `runs` gets built differs.\n */\nasync function runOne(\n ctx: DerivationDispatchCtx,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n spec: any,\n strategyHash: string,\n executorClass: typeof DerivationExecutor,\n run: { input: Record<string, unknown> & { id: string }; base: Record<string, unknown>; runId: string; version: number },\n): Promise<void> {\n const { derivationSource, adapter, vault, getDEK, storeCiphertext, dispatchCtx, trackPut } = ctx\n const execCtx = { vault: derivationSource.getReadOnlyFacade() }\n const outCtx = dispatchCtx({ collection: spec.source, id: run.runId })\n const result = await executorClass.run(spec, run.input, run.version, strategyHash, execCtx)\n for (const key of Object.keys(spec.outputs)) {\n const out = result.outputs[key]\n if (!out) continue\n if (out.kind === 'failed') {\n const err = out.error\n if (spec.strict) throw err\n console.warn(`[derivation] output \"${key}\" for source \"${spec.source}\" id=\"${run.runId}\" failed:`, err)\n continue\n }\n const outSpec = spec.outputs[key]\n if (!outSpec) continue\n const outputCollection = derivationSource.getCollection(outSpec.collection)\n // If we're inside a multi-record transaction, register\n // derived writes as side-effect ops on the active ctx\n // BEFORE they fire. `revertExecuted` walks `_executed` in\n // reverse on rollback, so capturing the pre-write envelope\n // here lets a later mid-batch failure restore this output's\n // prior state alongside the source op. Outside a transaction\n // the context is null and tracking is skipped.\n const txCtx = derivationSource.getActiveTxContext()\n\n // ── Array-shape branch ─────────────────────────────────\n if (out.kind === 'array') {\n // Load the prior key set from the fanout sidecar.\n const { loadFanoutSidecar, saveFanoutSidecar } = await import('./fanout-sidecar.js')\n const prior = await loadFanoutSidecar(adapter, vault, spec.source, run.runId, key, getDEK, storeCiphertext)\n const prevKeys = new Set<string>(prior?.keys ?? [])\n const newKeysList = out.entries.map((e: { key: string }) => e.key)\n const newKeysSet = new Set<string>(newKeysList)\n\n // Diff — delete keys that were in prev but not in new.\n for (const k of prevKeys) {\n if (newKeysSet.has(k)) continue\n await outputCollection._internalDelete(k, txCtx)\n }\n\n // Upsert every entry in the new set. (Slice 1: no\n // identity-skip optimisation; write every row, idempotent\n // at the (collection, id) level.)\n for (const entry of out.entries) {\n if (txCtx !== null) {\n trackPut(txCtx, outSpec.collection, entry.key, await adapter.get(vault, outSpec.collection, entry.key))\n }\n await putDerivedOutput(outputCollection, entry.key, entry.value, outCtx, { source: 'derived' })\n }\n\n // Persist the new key set last, for failure-mode symmetry.\n await saveFanoutSidecar(adapter, vault, {\n source: spec.source,\n sourceId: run.runId,\n outputKey: key,\n outputCollection: outSpec.collection,\n keys: newKeysList,\n }, getDEK, storeCiphertext)\n continue\n }\n\n // ── Record-shape branch ────────────────────────────────\n if (out.skipped === true) {\n // Optional output returned null. Delete the\n // previously-emitted output at this id, if any. Routed\n // through `_internalDelete` so a user-registered\n // `onDelete` on the output collection does NOT\n // fire — this is a system-internal tombstone, not a\n // user-initiated delete. The txCtx hookup captures the\n // prior envelope inside `_internalDelete` for rollback\n // symmetry; delete-of-absent is a silent no-op.\n await outputCollection._internalDelete(run.runId, txCtx)\n continue\n }\n\n // ── Self-write reverse-denorm ───────────────────────────\n // An output back to its own source: patch ONLY the declared\n // `denorm` fields onto the raw stored record, never the whole\n // value (which would clobber user fields / i18n maps and carries\n // the executor's `_derivedFrom` tag). If the patch changes\n // nothing, skip the write — that value-equality is the cycle\n // guard: the self-write re-fires the source-path derivation,\n // which recomputes identical fields and terminates here.\n if (outSpec.shape === 'record' && outSpec.denorm !== undefined && outSpec.collection === spec.source) {\n const value = out.value\n const patched: Record<string, unknown> = { ...run.base }\n let changed = false\n for (const f of outSpec.denorm) {\n if (!selfWriteFieldEqual(run.base[f], value[f])) {\n patched[f] = value[f]\n changed = true\n }\n }\n if (!changed) continue // cycle guard — nothing to write\n if (txCtx !== null) {\n trackPut(txCtx, outSpec.collection, run.runId, await adapter.get(vault, outSpec.collection, run.runId))\n }\n await putDerivedOutput(outputCollection, run.runId, patched, outCtx, { source: 'derived' })\n continue\n }\n\n // ── Normal record output (separate output collection) ──\n if (txCtx !== null) {\n trackPut(txCtx, outSpec.collection, run.runId, await adapter.get(vault, outSpec.collection, run.runId))\n }\n await putDerivedOutput(outputCollection, run.runId, out.value, outCtx, { source: 'derived' })\n }\n}\n\n/**\n * Trigger fan-out for a DELETED parent record (#1249, spec §8). Distinct from\n * the \"record-shape derivations not dispatched on delete\" rule — that is\n * about deleting a SOURCE record; this fires when a TRIGGER collection's\n * record is deleted, re-deriving source records that still exist. Pairs\n * evaluate against the tombstoned record's values; matched sources re-derive\n * through the normal executor (their derive() reads the now-absent parent\n * and decides what that means — the engine never cascades deletes).\n */\nexport async function dispatchTriggerDerivationsOnDelete(\n ctx: DerivationDispatchCtx, id: string, deleted: Record<string, unknown>,\n): Promise<void> {\n const { derivationSource, collectionName } = ctx\n const registry = derivationSource.registry()\n const strategies = registry.strategiesForSource(collectionName)\n if (strategies.length === 0) return\n let executorClass: typeof DerivationExecutor | null = null\n for (const { spec, strategyHash, triggers } of strategies) {\n if (spec.rollup) continue // rollup-on-delete already exists\n if (spec.source === collectionName) continue // source delete: existing rule, untouched\n if (spec.sources?.includes(collectionName)) continue // declared sibling source: same exclusion as the write path's isSibling check\n const entries = triggers.filter((t) => t.collection === collectionName)\n if (entries.length === 0) continue\n const mode = typeof spec.lifecycle === 'string' ? spec.lifecycle : spec.lifecycle.mode\n const srcColl = derivationSource.getCollection(spec.source)\n const matched = new Set<string>()\n for (const trigger of entries) {\n const tuple = tupleFromWritten(trigger.match, id, deleted)\n if (tuple === null) continue\n const ids = await srcColl._findMatchingCompositeIds(tuple)\n if (trigger.maxFanout !== undefined && ids.length > trigger.maxFanout) {\n throw new DerivationCapExceededError(\n `triggerBy ${collectionName}→${spec.source} [${trigger.match.map(p => p.to).join(',')}] (delete)`,\n ids.length, trigger.maxFanout)\n }\n for (const sid of ids) matched.add(sid)\n }\n if (matched.size === 0) continue\n if (mode !== 'eager') { for (const sid of matched) await markStale(registry, spec, sid); continue }\n if (executorClass === null) ({ DerivationExecutor: executorClass } = await import('./executor.js'))\n for (const sid of matched) {\n const raw = await srcColl._getStoredRecord(sid)\n if (raw === null) continue\n await runOne(ctx, spec, strategyHash, executorClass, { input: { ...raw, id: sid }, base: raw, runId: sid, version: 0 })\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/with-formula/derivations/dispatch.ts"],"sourcesContent":["/**\n * Derivation dispatch, lifted out of the kernel spine (#842 part b).\n *\n * `Collection` carried ~380 lines of dispatch logic that belongs to the\n * derivation service, not to the always-on kernel. These are free functions\n * taking an explicit context so the spine keeps only a thin delegator.\n *\n * ## How the spine reaches this module\n *\n * By dynamic `import()`, never a static one. `check-architecture.mjs`'s\n * `port-layering` check forbids the spine from statically importing a\n * `with-*` service; `collection.ts` is grandfathered for ten\n * `with-formula/*` specifiers, but that list is frozen per file and a new\n * one is a violation. A dynamic import is the sanctioned S4 gate recipe, and\n * it keeps this chunk out of the floor bundle for consumers who never fire a\n * derivation.\n *\n * The `Collection` type reference below points back at the spine. That is\n * inward (service → kernel) and so allowed, and it is TYPE-ONLY — erased at\n * build, so no runtime cycle exists.\n *\n * @internal\n */\n\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { TxContext } from '../../with-commit/tx/transaction.js'\nimport type { DerivationRegistry } from './registry.js'\nimport type { ReadOnlyVaultFacade } from '../../with-audit/guards/types.js'\nimport type { Collection } from '../../kernel/collection.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { EncryptedEnvelope } from '../../kernel/types.js'\nimport type { ledgerAuditHook, WaveContext } from '../../kernel/via/dispatch.js'\nimport { putDerivedOutput, selfWriteFieldEqual } from '../../kernel/via/dispatch.js'\nimport { DerivationCapExceededError } from '../../kernel/errors.js'\nimport { tupleFromWritten, sameTuple } from './trigger-match.js'\nimport { markStale } from './stale.js'\nimport type { DerivationExecutor } from './executor.js'\n\n/** The per-write dispatch context threaded into `putDerivedOutput`. */\ninterface DispatchCtx {\n emit: (e: string, p: unknown) => void\n source: { readonly collection: string; readonly id: string }\n audit: ReturnType<typeof ledgerAuditHook>\n}\n\n/**\n * What the dispatchers need from the collection that owns them. Passing this\n * explicitly is what lets the logic live outside the class.\n */\nexport interface DerivationDeleteCtx {\n readonly derivationSource: {\n registry(): DerivationRegistry\n getCollection(name: string): Collection<Record<string, unknown>>\n getActiveTxContext(): TxContext | null\n getReadOnlyFacade(): ReadOnlyVaultFacade\n }\n /** The collection the delete happened in. */\n readonly collectionName: string\n readonly adapter: NoydbStore\n readonly vault: string\n readonly getDEK: (collectionName: string) => Promise<EnclaveKey>\n readonly storeCiphertext: boolean\n}\n\n/**\n * Erase the derived rows a deleted source record fanned out to, and drop the\n * fan-out sidecars that recorded them.\n *\n * @param eraseRecordShapeToo also erase same-id `shape: 'record'` outputs.\n * Restricted to a source-triggered strategy writing into a DIFFERENT\n * collection — the self-denorm case would re-delete the record just\n * tombstoned, and triggerBy/sibling strategies derive under a different id.\n * @returns rows ACTUALLY deleted, not edges visited (#622).\n */\nexport async function dispatchArrayDerivationsOnDelete(\n ctx: DerivationDeleteCtx,\n id: string,\n eraseRecordShapeToo = false,\n): Promise<number> {\n const { derivationSource, collectionName, adapter, vault, getDEK, storeCiphertext } = ctx\n\n const strategies = derivationSource.registry().strategiesForSource(collectionName)\n if (strategies.length === 0) return 0\n\n const { loadFanoutSidecar, deleteFanoutSidecar } = await import('./fanout-sidecar.js')\n const txCtx = derivationSource.getActiveTxContext()\n let erased = 0\n\n for (const { spec } of strategies) {\n for (const [outputKey, outSpec] of Object.entries(spec.outputs)) {\n if (outSpec.shape === 'record') {\n if (eraseRecordShapeToo && spec.source === collectionName && outSpec.collection !== collectionName) {\n if (await derivationSource.getCollection(outSpec.collection)._internalDelete(id, txCtx)) erased += 1\n }\n continue\n }\n\n const sidecar = await loadFanoutSidecar(adapter, vault, spec.source, id, outputKey, getDEK, storeCiphertext)\n if (!sidecar) continue\n\n const outputCollection = derivationSource.getCollection(outSpec.collection)\n for (const derivedId of sidecar.keys) {\n if (await outputCollection._internalDelete(derivedId, txCtx)) erased += 1\n }\n await deleteFanoutSidecar(adapter, vault, spec.source, id, outputKey)\n }\n }\n\n return erased\n}\n\n/** Adds what the on-write derivation dispatch needs beyond the delete path. */\nexport interface DerivationDispatchCtx extends DerivationDeleteCtx {\n /** Via pipeline, for decoding the stored form back to canonical money shape. */\n readonly via: { canonicalizeStored(r: Record<string, unknown>): Record<string, unknown> } | undefined\n /** Recompute a rollup aggregate onto a parent. Stays on the spine — it walks collections. */\n readonly recomputeRollup: (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n spec: any,\n parentId: string,\n source: { readonly collection: string; readonly id: string },\n wave?: WaveContext,\n ) => Promise<unknown>\n /** Builds the per-write dispatch context threaded into `putDerivedOutput`. */\n readonly dispatchCtx: (source: { readonly collection: string; readonly id: string }) => DispatchCtx\n /** Registers a write on the active transaction for rollback. */\n readonly trackPut: (txCtx: TxContext, collectionName: string, id: string, priorEnvelope: EncryptedEnvelope | null) => void\n}\n\n/**\n * Fire registered derivation strategies for this source collection.\n *\n * Eager mode runs `derive` inline and writes each output; lazy mode records a\n * stale mark for cold-session recompute. Rollups are handled here rather than\n * by the executor — a write to the child recomputes the parent aggregate, and\n * a write to the parent recomputes its own.\n */\nexport async function dispatchDerivations(\n ctx: DerivationDispatchCtx,\n id: string,\n record: Record<string, unknown>,\n version: number,\n wave?: WaveContext,\n /** The pre-write record at `id` in THIS collection (undefined = not\n * captured — no field-match trigger, or a sync-applied wave; null =\n * captured and absent, i.e. a create). Only a `Record` re-fires the OLD\n * matched set alongside the new one (spec §7). */\n prior?: Record<string, unknown> | null,\n): Promise<void> {\n const { derivationSource, collectionName, via, recomputeRollup } = ctx\n\n // `record` is the stored form here (post-quantize) — decode so\n // derive(source, ctx) sees the canonical money shape.\n const incoming = (via ? via.canonicalizeStored(record) : record)\n if (incoming && typeof incoming === 'object' && '_derivedFrom' in incoming) return\n // `prior` is the raw stored pre-write record — canonicalize it the same\n // way as `incoming` before it feeds `tupleFromWritten` below, or a\n // via-shaped match field (e.g. a money field) compares canonical against\n // raw and never matches the old tuple (Min 5).\n const canonicalPrior = prior != null ? (via ? via.canonicalizeStored(prior) : prior) : prior\n const registry = derivationSource.registry()\n const strategies = registry.strategiesForSource(collectionName)\n if (strategies.length === 0) return\n // Dynamic-import the executor only on the first eager-mode\n // dispatch. Lazy-mode dispatches use `markStale` (a pure helper)\n // which doesn't reach into the executor at all. Keeps the\n // derivation executor chunk out of the floor bundle for any\n // consumer that doesn't fire an eager derivation.\n let executorClass: typeof DerivationExecutor | null = null\n for (const { spec, strategyHash, triggers } of strategies) {\n const mode = typeof spec.lifecycle === 'string' ? spec.lifecycle : spec.lifecycle.mode\n\n // Rollup: a write to the child `from` recomputes the\n // parent at id child[key]; a write to the parent (source = into)\n // recomputes its own aggregate. Handled here (the executor is not run).\n if (spec.rollup) {\n if (mode !== 'eager') continue\n const asParentId = (v: unknown): string | null =>\n (typeof v === 'string' || typeof v === 'number') ? String(v) : null\n let parentId: string | null\n // #1257 — a child that RE-PARENTS strands the old parent's aggregate\n // unless the old key is recomputed too. Reads the prior stored record\n // (canonicalized alongside `incoming`, so a via-shaped key compares like\n // for like). `undefined` prior — a sync-applied wave write or a tiers\n // restore, which do not thread it — degrades to new-parent-only, the\n // pre-#1257 behaviour, rather than guessing.\n let oldParentId: string | null = null\n if (collectionName === spec.rollup.from) {\n parentId = asParentId(incoming[spec.rollup.key])\n const priorKey = canonicalPrior != null ? asParentId(canonicalPrior[spec.rollup.key]) : null\n if (priorKey !== null && priorKey !== parentId) oldParentId = priorKey\n } else {\n parentId = id // a write to the parent recomputes its own aggregate\n }\n if (parentId !== null) await recomputeRollup(spec, parentId, { collection: collectionName, id }, wave)\n // The old parent is recomputed AFTER the new one so a `maxFanout`-style\n // failure on the new parent surfaces before extra work is spent.\n if (oldParentId !== null) await recomputeRollup(spec, oldParentId, { collection: collectionName, id }, wave)\n continue\n }\n\n // Determine how `collectionName` triggers this strategy, and build the list\n // of source records to (re-)derive:\n // • source — re-derive the written record itself (same-id).\n // • sources[] — re-derive the PRIMARY source at the same id.\n // • triggerBy — FK fan-out: re-derive every source record\n // whose `on` field equals the written parent's id.\n // `input` is passed to derive(); `base` is the raw stored source record\n // used as the patch base for a self-write reverse-denorm output.\n const isSource = spec.source === collectionName\n const isSibling = !isSource && (spec.sources?.includes(collectionName) ?? false)\n const triggerEntries = !isSource && !isSibling\n ? triggers.filter((t) => t.collection === collectionName)\n : []\n\n const runs: Array<{\n input: Record<string, unknown> & { id: string }\n base: Record<string, unknown>\n runId: string\n version: number\n }> = []\n\n if (isSource) {\n runs.push({ input: { ...incoming, id }, base: incoming, runId: id, version })\n } else if (isSibling) {\n const p = await derivationSource.getCollection(spec.source).get(id)\n if (p !== null && p !== undefined) {\n // Raw base for a (rare) sibling self-write; falls back to the\n // resolved primary if the raw read misses.\n const raw = await derivationSource.getCollection(spec.source)._getStoredRecord(id)\n runs.push({ input: { ...p, id }, base: raw ?? p, runId: id, version: 0 })\n }\n } else if (triggerEntries.length > 0) {\n const srcColl = derivationSource.getCollection(spec.source)\n const matched = new Set<string>()\n for (const trigger of triggerEntries) {\n // Union fan-out (spec §7): an update changing any matched component\n // must re-fire BOTH the old and the new matched set, or the old\n // set's derived output goes stale (never re-derived once it no\n // longer matches). `prior` is only ever a `Record` here — a wave\n // dispatch and a create both skip the old tuple outright.\n const tuples = [tupleFromWritten(trigger.match, id, incoming)]\n if (canonicalPrior != null && trigger.match.some((p) => p.from !== 'id')) {\n const old = tupleFromWritten(trigger.match, id, canonicalPrior)\n if (!sameTuple(old, tuples[0]!)) tuples.push(old)\n }\n const ids = new Set<string>()\n for (const tuple of tuples) {\n if (tuple === null) continue // a from-value is absent/non-scalar: matches nothing\n for (const sid of await srcColl._findMatchingCompositeIds(tuple)) ids.add(sid)\n }\n if (trigger.maxFanout !== undefined && ids.size > trigger.maxFanout) {\n throw new DerivationCapExceededError(\n `triggerBy ${collectionName}→${spec.source} [${trigger.match.map(p => p.to).join(',')}]`,\n ids.size, trigger.maxFanout)\n }\n for (const sid of ids) matched.add(sid)\n }\n for (const sid of matched) {\n const raw = await srcColl._getStoredRecord(sid)\n if (raw === null) continue\n runs.push({ input: { ...raw, id: sid }, base: raw, runId: sid, version: 0 })\n }\n }\n\n if (runs.length === 0) continue\n\n if (mode !== 'eager') {\n for (const run of runs) await markStale(registry, spec, run.runId)\n continue\n }\n\n if (executorClass === null) {\n ;({ DerivationExecutor: executorClass } = await import('./executor.js'))\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n for (const run of runs) await runOne(ctx, spec as any, strategyHash, executorClass, run)\n }\n}\n\n/**\n * Run the executor for one (spec, run) pair and write its outputs. Shared by\n * `dispatchDerivations`' eager loop and `dispatchTriggerDerivationsOnDelete`\n * (#1249) — the write-out logic (array diff, record self-write reverse-denorm,\n * normal record output) is identical for a live write and a delete-triggered\n * re-derive; only how `runs` gets built differs.\n */\nasync function runOne(\n ctx: DerivationDispatchCtx,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n spec: any,\n strategyHash: string,\n executorClass: typeof DerivationExecutor,\n run: { input: Record<string, unknown> & { id: string }; base: Record<string, unknown>; runId: string; version: number },\n): Promise<void> {\n const { derivationSource, adapter, vault, getDEK, storeCiphertext, dispatchCtx, trackPut } = ctx\n const execCtx = { vault: derivationSource.getReadOnlyFacade() }\n const outCtx = dispatchCtx({ collection: spec.source, id: run.runId })\n const result = await executorClass.run(spec, run.input, run.version, strategyHash, execCtx)\n for (const key of Object.keys(spec.outputs)) {\n const out = result.outputs[key]\n if (!out) continue\n if (out.kind === 'failed') {\n const err = out.error\n if (spec.strict) throw err\n console.warn(`[derivation] output \"${key}\" for source \"${spec.source}\" id=\"${run.runId}\" failed:`, err)\n continue\n }\n const outSpec = spec.outputs[key]\n if (!outSpec) continue\n const outputCollection = derivationSource.getCollection(outSpec.collection)\n // If we're inside a multi-record transaction, register\n // derived writes as side-effect ops on the active ctx\n // BEFORE they fire. `revertExecuted` walks `_executed` in\n // reverse on rollback, so capturing the pre-write envelope\n // here lets a later mid-batch failure restore this output's\n // prior state alongside the source op. Outside a transaction\n // the context is null and tracking is skipped.\n const txCtx = derivationSource.getActiveTxContext()\n\n // ── Array-shape branch ─────────────────────────────────\n if (out.kind === 'array') {\n // Load the prior key set from the fanout sidecar.\n const { loadFanoutSidecar, saveFanoutSidecar } = await import('./fanout-sidecar.js')\n const prior = await loadFanoutSidecar(adapter, vault, spec.source, run.runId, key, getDEK, storeCiphertext)\n const prevKeys = new Set<string>(prior?.keys ?? [])\n const newKeysList = out.entries.map((e: { key: string }) => e.key)\n const newKeysSet = new Set<string>(newKeysList)\n\n // Diff — delete keys that were in prev but not in new.\n for (const k of prevKeys) {\n if (newKeysSet.has(k)) continue\n await outputCollection._internalDelete(k, txCtx)\n }\n\n // Upsert every entry in the new set. (Slice 1: no\n // identity-skip optimisation; write every row, idempotent\n // at the (collection, id) level.)\n for (const entry of out.entries) {\n if (txCtx !== null) {\n trackPut(txCtx, outSpec.collection, entry.key, await adapter.get(vault, outSpec.collection, entry.key))\n }\n await putDerivedOutput(outputCollection, entry.key, entry.value, outCtx, { source: 'derived' })\n }\n\n // Persist the new key set last, for failure-mode symmetry.\n await saveFanoutSidecar(adapter, vault, {\n source: spec.source,\n sourceId: run.runId,\n outputKey: key,\n outputCollection: outSpec.collection,\n keys: newKeysList,\n }, getDEK, storeCiphertext)\n continue\n }\n\n // ── Record-shape branch ────────────────────────────────\n if (out.skipped === true) {\n // Optional output returned null. Delete the\n // previously-emitted output at this id, if any. Routed\n // through `_internalDelete` so a user-registered\n // `onDelete` on the output collection does NOT\n // fire — this is a system-internal tombstone, not a\n // user-initiated delete. The txCtx hookup captures the\n // prior envelope inside `_internalDelete` for rollback\n // symmetry; delete-of-absent is a silent no-op.\n await outputCollection._internalDelete(run.runId, txCtx)\n continue\n }\n\n // ── Self-write reverse-denorm ───────────────────────────\n // An output back to its own source: patch ONLY the declared\n // `denorm` fields onto the raw stored record, never the whole\n // value (which would clobber user fields / i18n maps and carries\n // the executor's `_derivedFrom` tag). If the patch changes\n // nothing, skip the write — that value-equality is the cycle\n // guard: the self-write re-fires the source-path derivation,\n // which recomputes identical fields and terminates here.\n if (outSpec.shape === 'record' && outSpec.denorm !== undefined && outSpec.collection === spec.source) {\n const value = out.value\n const patched: Record<string, unknown> = { ...run.base }\n let changed = false\n for (const f of outSpec.denorm) {\n if (!selfWriteFieldEqual(run.base[f], value[f])) {\n patched[f] = value[f]\n changed = true\n }\n }\n if (!changed) continue // cycle guard — nothing to write\n if (txCtx !== null) {\n trackPut(txCtx, outSpec.collection, run.runId, await adapter.get(vault, outSpec.collection, run.runId))\n }\n await putDerivedOutput(outputCollection, run.runId, patched, outCtx, { source: 'derived' })\n continue\n }\n\n // ── Normal record output (separate output collection) ──\n if (txCtx !== null) {\n trackPut(txCtx, outSpec.collection, run.runId, await adapter.get(vault, outSpec.collection, run.runId))\n }\n await putDerivedOutput(outputCollection, run.runId, out.value, outCtx, { source: 'derived' })\n }\n}\n\n/**\n * Trigger fan-out for a DELETED parent record (#1249, spec §8). Distinct from\n * the \"record-shape derivations not dispatched on delete\" rule — that is\n * about deleting a SOURCE record; this fires when a TRIGGER collection's\n * record is deleted, re-deriving source records that still exist. Pairs\n * evaluate against the tombstoned record's values; matched sources re-derive\n * through the normal executor (their derive() reads the now-absent parent\n * and decides what that means — the engine never cascades deletes).\n */\nexport async function dispatchTriggerDerivationsOnDelete(\n ctx: DerivationDispatchCtx, id: string, deleted: Record<string, unknown>,\n): Promise<void> {\n const { derivationSource, collectionName } = ctx\n const registry = derivationSource.registry()\n const strategies = registry.strategiesForSource(collectionName)\n if (strategies.length === 0) return\n let executorClass: typeof DerivationExecutor | null = null\n for (const { spec, strategyHash, triggers } of strategies) {\n if (spec.rollup) continue // rollup-on-delete already exists\n if (spec.source === collectionName) continue // source delete: existing rule, untouched\n if (spec.sources?.includes(collectionName)) continue // declared sibling source: same exclusion as the write path's isSibling check\n const entries = triggers.filter((t) => t.collection === collectionName)\n if (entries.length === 0) continue\n const mode = typeof spec.lifecycle === 'string' ? spec.lifecycle : spec.lifecycle.mode\n const srcColl = derivationSource.getCollection(spec.source)\n const matched = new Set<string>()\n for (const trigger of entries) {\n const tuple = tupleFromWritten(trigger.match, id, deleted)\n if (tuple === null) continue\n const ids = await srcColl._findMatchingCompositeIds(tuple)\n if (trigger.maxFanout !== undefined && ids.length > trigger.maxFanout) {\n throw new DerivationCapExceededError(\n `triggerBy ${collectionName}→${spec.source} [${trigger.match.map(p => p.to).join(',')}] (delete)`,\n ids.length, trigger.maxFanout)\n }\n for (const sid of ids) matched.add(sid)\n }\n if (matched.size === 0) continue\n if (mode !== 'eager') { for (const sid of matched) await markStale(registry, spec, sid); continue }\n if (executorClass === null) ({ DerivationExecutor: executorClass } = await import('./executor.js'))\n for (const sid of matched) {\n const raw = await srcColl._getStoredRecord(sid)\n if (raw === null) continue\n await runOne(ctx, spec, strategyHash, executorClass, { input: { ...raw, id: sid }, base: raw, runId: sid, version: 0 })\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0EA,eAAsB,iCACpB,KACA,IACA,sBAAsB,OACL;AACjB,QAAM,EAAE,kBAAkB,gBAAgB,SAAS,OAAO,QAAQ,gBAAgB,IAAI;AAEtF,QAAM,aAAa,iBAAiB,SAAS,EAAE,oBAAoB,cAAc;AACjF,MAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,QAAM,EAAE,mBAAmB,oBAAoB,IAAI,MAAM,OAAO,8BAAqB;AACrF,QAAM,QAAQ,iBAAiB,mBAAmB;AAClD,MAAI,SAAS;AAEb,aAAW,EAAE,KAAK,KAAK,YAAY;AACjC,eAAW,CAAC,WAAW,OAAO,KAAK,OAAO,QAAQ,KAAK,OAAO,GAAG;AAC/D,UAAI,QAAQ,UAAU,UAAU;AAC9B,YAAI,uBAAuB,KAAK,WAAW,kBAAkB,QAAQ,eAAe,gBAAgB;AAClG,cAAI,MAAM,iBAAiB,cAAc,QAAQ,UAAU,EAAE,gBAAgB,IAAI,KAAK,EAAG,WAAU;AAAA,QACrG;AACA;AAAA,MACF;AAEA,YAAM,UAAU,MAAM,kBAAkB,SAAS,OAAO,KAAK,QAAQ,IAAI,WAAW,QAAQ,eAAe;AAC3G,UAAI,CAAC,QAAS;AAEd,YAAM,mBAAmB,iBAAiB,cAAc,QAAQ,UAAU;AAC1E,iBAAW,aAAa,QAAQ,MAAM;AACpC,YAAI,MAAM,iBAAiB,gBAAgB,WAAW,KAAK,EAAG,WAAU;AAAA,MAC1E;AACA,YAAM,oBAAoB,SAAS,OAAO,KAAK,QAAQ,IAAI,SAAS;AAAA,IACtE;AAAA,EACF;AAEA,SAAO;AACT;AA4BA,eAAsB,oBACpB,KACA,IACA,QACA,SACA,MAKA,OACe;AACf,QAAM,EAAE,kBAAkB,gBAAgB,KAAK,gBAAgB,IAAI;AAInE,QAAM,WAAY,MAAM,IAAI,mBAAmB,MAAM,IAAI;AACzD,MAAI,YAAY,OAAO,aAAa,YAAY,kBAAkB,SAAU;AAK5E,QAAM,iBAAiB,SAAS,OAAQ,MAAM,IAAI,mBAAmB,KAAK,IAAI,QAAS;AACvF,QAAM,WAAW,iBAAiB,SAAS;AAC3C,QAAM,aAAa,SAAS,oBAAoB,cAAc;AAC9D,MAAI,WAAW,WAAW,EAAG;AAM7B,MAAI,gBAAkD;AACtD,aAAW,EAAE,MAAM,cAAc,SAAS,KAAK,YAAY;AACzD,UAAM,OAAO,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY,KAAK,UAAU;AAKlF,QAAI,KAAK,QAAQ;AACf,UAAI,SAAS,QAAS;AACtB,YAAM,aAAa,CAAC,MACjB,OAAO,MAAM,YAAY,OAAO,MAAM,WAAY,OAAO,CAAC,IAAI;AACjE,UAAI;AAOJ,UAAI,cAA6B;AACjC,UAAI,mBAAmB,KAAK,OAAO,MAAM;AACvC,mBAAW,WAAW,SAAS,KAAK,OAAO,GAAG,CAAC;AAC/C,cAAM,WAAW,kBAAkB,OAAO,WAAW,eAAe,KAAK,OAAO,GAAG,CAAC,IAAI;AACxF,YAAI,aAAa,QAAQ,aAAa,SAAU,eAAc;AAAA,MAChE,OAAO;AACL,mBAAW;AAAA,MACb;AACA,UAAI,aAAa,KAAM,OAAM,gBAAgB,MAAM,UAAU,EAAE,YAAY,gBAAgB,GAAG,GAAG,IAAI;AAGrG,UAAI,gBAAgB,KAAM,OAAM,gBAAgB,MAAM,aAAa,EAAE,YAAY,gBAAgB,GAAG,GAAG,IAAI;AAC3G;AAAA,IACF;AAUA,UAAM,WAAW,KAAK,WAAW;AACjC,UAAM,YAAY,CAAC,aAAa,KAAK,SAAS,SAAS,cAAc,KAAK;AAC1E,UAAM,iBAAiB,CAAC,YAAY,CAAC,YACjC,SAAS,OAAO,CAAC,MAAM,EAAE,eAAe,cAAc,IACtD,CAAC;AAEL,UAAM,OAKD,CAAC;AAEN,QAAI,UAAU;AACZ,WAAK,KAAK,EAAE,OAAO,EAAE,GAAG,UAAU,GAAG,GAAG,MAAM,UAAU,OAAO,IAAI,QAAQ,CAAC;AAAA,IAC9E,WAAW,WAAW;AACpB,YAAM,IAAI,MAAM,iBAAiB,cAAc,KAAK,MAAM,EAAE,IAAI,EAAE;AAClE,UAAI,MAAM,QAAQ,MAAM,QAAW;AAGjC,cAAM,MAAM,MAAM,iBAAiB,cAAc,KAAK,MAAM,EAAE,iBAAiB,EAAE;AACjF,aAAK,KAAK,EAAE,OAAO,EAAE,GAAG,GAAG,GAAG,GAAG,MAAM,OAAO,GAAG,OAAO,IAAI,SAAS,EAAE,CAAC;AAAA,MAC1E;AAAA,IACF,WAAW,eAAe,SAAS,GAAG;AACpC,YAAM,UAAU,iBAAiB,cAAc,KAAK,MAAM;AAC1D,YAAM,UAAU,oBAAI,IAAY;AAChC,iBAAW,WAAW,gBAAgB;AAMpC,cAAM,SAAS,CAAC,iBAAiB,QAAQ,OAAO,IAAI,QAAQ,CAAC;AAC7D,YAAI,kBAAkB,QAAQ,QAAQ,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,GAAG;AACxE,gBAAM,MAAM,iBAAiB,QAAQ,OAAO,IAAI,cAAc;AAC9D,cAAI,CAAC,UAAU,KAAK,OAAO,CAAC,CAAE,EAAG,QAAO,KAAK,GAAG;AAAA,QAClD;AACA,cAAM,MAAM,oBAAI,IAAY;AAC5B,mBAAW,SAAS,QAAQ;AAC1B,cAAI,UAAU,KAAM;AACpB,qBAAW,OAAO,MAAM,QAAQ,0BAA0B,KAAK,EAAG,KAAI,IAAI,GAAG;AAAA,QAC/E;AACA,YAAI,QAAQ,cAAc,UAAa,IAAI,OAAO,QAAQ,WAAW;AACnE,gBAAM,IAAI;AAAA,YACR,aAAa,cAAc,SAAI,KAAK,MAAM,KAAK,QAAQ,MAAM,IAAI,OAAK,EAAE,EAAE,EAAE,KAAK,GAAG,CAAC;AAAA,YACrF,IAAI;AAAA,YAAM,QAAQ;AAAA,UAAS;AAAA,QAC/B;AACA,mBAAW,OAAO,IAAK,SAAQ,IAAI,GAAG;AAAA,MACxC;AACA,iBAAW,OAAO,SAAS;AACzB,cAAM,MAAM,MAAM,QAAQ,iBAAiB,GAAG;AAC9C,YAAI,QAAQ,KAAM;AAClB,aAAK,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK,IAAI,IAAI,GAAG,MAAM,KAAK,OAAO,KAAK,SAAS,EAAE,CAAC;AAAA,MAC7E;AAAA,IACF;AAEA,QAAI,KAAK,WAAW,EAAG;AAEvB,QAAI,SAAS,SAAS;AACpB,iBAAW,OAAO,KAAM,OAAM,UAAU,UAAU,MAAM,IAAI,KAAK;AACjE;AAAA,IACF;AAEA,QAAI,kBAAkB,MAAM;AAC1B;AAAC,OAAC,EAAE,oBAAoB,cAAc,IAAI,MAAM,OAAO,wBAAe;AAAA,IACxE;AAGA,eAAW,OAAO,KAAM,OAAM,OAAO,KAAK,MAAa,cAAc,eAAe,GAAG;AAAA,EACzF;AACF;AASA,eAAe,OACb,KAEA,MACA,cACA,eACA,KACe;AACf,QAAM,EAAE,kBAAkB,SAAS,OAAO,QAAQ,iBAAiB,aAAa,SAAS,IAAI;AAC7F,QAAM,UAAU,EAAE,OAAO,iBAAiB,kBAAkB,EAAE;AAC9D,QAAM,SAAS,YAAY,EAAE,YAAY,KAAK,QAAQ,IAAI,IAAI,MAAM,CAAC;AACrE,QAAM,SAAS,MAAM,cAAc,IAAI,MAAM,IAAI,OAAO,IAAI,SAAS,cAAc,OAAO;AAC1F,aAAW,OAAO,OAAO,KAAK,KAAK,OAAO,GAAG;AAC3C,UAAM,MAAM,OAAO,QAAQ,GAAG;AAC9B,QAAI,CAAC,IAAK;AACV,QAAI,IAAI,SAAS,UAAU;AACzB,YAAM,MAAM,IAAI;AAChB,UAAI,KAAK,OAAQ,OAAM;AACvB,cAAQ,KAAK,wBAAwB,GAAG,iBAAiB,KAAK,MAAM,SAAS,IAAI,KAAK,aAAa,GAAG;AACtG;AAAA,IACF;AACA,UAAM,UAAU,KAAK,QAAQ,GAAG;AAChC,QAAI,CAAC,QAAS;AACd,UAAM,mBAAmB,iBAAiB,cAAc,QAAQ,UAAU;AAQ1E,UAAM,QAAQ,iBAAiB,mBAAmB;AAGlD,QAAI,IAAI,SAAS,SAAS;AAExB,YAAM,EAAE,mBAAmB,kBAAkB,IAAI,MAAM,OAAO,8BAAqB;AACnF,YAAM,QAAQ,MAAM,kBAAkB,SAAS,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,QAAQ,eAAe;AAC1G,YAAM,WAAW,IAAI,IAAY,OAAO,QAAQ,CAAC,CAAC;AAClD,YAAM,cAAc,IAAI,QAAQ,IAAI,CAAC,MAAuB,EAAE,GAAG;AACjE,YAAM,aAAa,IAAI,IAAY,WAAW;AAG9C,iBAAW,KAAK,UAAU;AACxB,YAAI,WAAW,IAAI,CAAC,EAAG;AACvB,cAAM,iBAAiB,gBAAgB,GAAG,KAAK;AAAA,MACjD;AAKA,iBAAW,SAAS,IAAI,SAAS;AAC/B,YAAI,UAAU,MAAM;AAClB,mBAAS,OAAO,QAAQ,YAAY,MAAM,KAAK,MAAM,QAAQ,IAAI,OAAO,QAAQ,YAAY,MAAM,GAAG,CAAC;AAAA,QACxG;AACA,cAAM,iBAAiB,kBAAkB,MAAM,KAAK,MAAM,OAAO,QAAQ,EAAE,QAAQ,UAAU,CAAC;AAAA,MAChG;AAGA,YAAM,kBAAkB,SAAS,OAAO;AAAA,QACtC,QAAQ,KAAK;AAAA,QACb,UAAU,IAAI;AAAA,QACd,WAAW;AAAA,QACX,kBAAkB,QAAQ;AAAA,QAC1B,MAAM;AAAA,MACR,GAAG,QAAQ,eAAe;AAC1B;AAAA,IACF;AAGA,QAAI,IAAI,YAAY,MAAM;AASxB,YAAM,iBAAiB,gBAAgB,IAAI,OAAO,KAAK;AACvD;AAAA,IACF;AAUA,QAAI,QAAQ,UAAU,YAAY,QAAQ,WAAW,UAAa,QAAQ,eAAe,KAAK,QAAQ;AACpG,YAAM,QAAQ,IAAI;AAClB,YAAM,UAAmC,EAAE,GAAG,IAAI,KAAK;AACvD,UAAI,UAAU;AACd,iBAAW,KAAK,QAAQ,QAAQ;AAC9B,YAAI,CAAC,oBAAoB,IAAI,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG;AAC/C,kBAAQ,CAAC,IAAI,MAAM,CAAC;AACpB,oBAAU;AAAA,QACZ;AAAA,MACF;AACA,UAAI,CAAC,QAAS;AACd,UAAI,UAAU,MAAM;AAClB,iBAAS,OAAO,QAAQ,YAAY,IAAI,OAAO,MAAM,QAAQ,IAAI,OAAO,QAAQ,YAAY,IAAI,KAAK,CAAC;AAAA,MACxG;AACA,YAAM,iBAAiB,kBAAkB,IAAI,OAAO,SAAS,QAAQ,EAAE,QAAQ,UAAU,CAAC;AAC1F;AAAA,IACF;AAGA,QAAI,UAAU,MAAM;AAClB,eAAS,OAAO,QAAQ,YAAY,IAAI,OAAO,MAAM,QAAQ,IAAI,OAAO,QAAQ,YAAY,IAAI,KAAK,CAAC;AAAA,IACxG;AACA,UAAM,iBAAiB,kBAAkB,IAAI,OAAO,IAAI,OAAO,QAAQ,EAAE,QAAQ,UAAU,CAAC;AAAA,EAC9F;AACF;AAWA,eAAsB,mCACpB,KAA4B,IAAY,SACzB;AACf,QAAM,EAAE,kBAAkB,eAAe,IAAI;AAC7C,QAAM,WAAW,iBAAiB,SAAS;AAC3C,QAAM,aAAa,SAAS,oBAAoB,cAAc;AAC9D,MAAI,WAAW,WAAW,EAAG;AAC7B,MAAI,gBAAkD;AACtD,aAAW,EAAE,MAAM,cAAc,SAAS,KAAK,YAAY;AACzD,QAAI,KAAK,OAAQ;AACjB,QAAI,KAAK,WAAW,eAAgB;AACpC,QAAI,KAAK,SAAS,SAAS,cAAc,EAAG;AAC5C,UAAM,UAAU,SAAS,OAAO,CAAC,MAAM,EAAE,eAAe,cAAc;AACtE,QAAI,QAAQ,WAAW,EAAG;AAC1B,UAAM,OAAO,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY,KAAK,UAAU;AAClF,UAAM,UAAU,iBAAiB,cAAc,KAAK,MAAM;AAC1D,UAAM,UAAU,oBAAI,IAAY;AAChC,eAAW,WAAW,SAAS;AAC7B,YAAM,QAAQ,iBAAiB,QAAQ,OAAO,IAAI,OAAO;AACzD,UAAI,UAAU,KAAM;AACpB,YAAM,MAAM,MAAM,QAAQ,0BAA0B,KAAK;AACzD,UAAI,QAAQ,cAAc,UAAa,IAAI,SAAS,QAAQ,WAAW;AACrE,cAAM,IAAI;AAAA,UACR,aAAa,cAAc,SAAI,KAAK,MAAM,KAAK,QAAQ,MAAM,IAAI,OAAK,EAAE,EAAE,EAAE,KAAK,GAAG,CAAC;AAAA,UACrF,IAAI;AAAA,UAAQ,QAAQ;AAAA,QAAS;AAAA,MACjC;AACA,iBAAW,OAAO,IAAK,SAAQ,IAAI,GAAG;AAAA,IACxC;AACA,QAAI,QAAQ,SAAS,EAAG;AACxB,QAAI,SAAS,SAAS;AAAE,iBAAW,OAAO,QAAS,OAAM,UAAU,UAAU,MAAM,GAAG;AAAG;AAAA,IAAS;AAClG,QAAI,kBAAkB,KAAM,EAAC,EAAE,oBAAoB,cAAc,IAAI,MAAM,OAAO,wBAAe;AACjG,eAAW,OAAO,SAAS;AACzB,YAAM,MAAM,MAAM,QAAQ,iBAAiB,GAAG;AAC9C,UAAI,QAAQ,KAAM;AAClB,YAAM,OAAO,KAAK,MAAM,cAAc,eAAe,EAAE,OAAO,EAAE,GAAG,KAAK,IAAI,IAAI,GAAG,MAAM,KAAK,OAAO,KAAK,SAAS,EAAE,CAAC;AAAA,IACxH;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import "./chunk-PZ5AY32C.js";
|
|
2
|
-
|
|
3
1
|
// src/with-formula/materialized-views/dispatch.ts
|
|
4
2
|
async function dispatchMaterializedViews(ctx, id, record, wave) {
|
|
5
3
|
const { materializedViewSource: source, collectionName, dispatchCtx } = ctx;
|
|
@@ -16,7 +14,7 @@ async function dispatchMaterializedViews(ctx, id, record, wave) {
|
|
|
16
14
|
if (wave?.seen(`mv\0${reg.spec.name}`)) continue;
|
|
17
15
|
if (executor === null) {
|
|
18
16
|
;
|
|
19
|
-
({ MaterializedViewExecutor: executor } = await import("./executor-
|
|
17
|
+
({ MaterializedViewExecutor: executor } = await import("./executor-5XJNV5H5.js"));
|
|
20
18
|
}
|
|
21
19
|
await executor.refresh(reg, {
|
|
22
20
|
getCollection: (name) => source.getCollection(name),
|
|
@@ -25,7 +23,7 @@ async function dispatchMaterializedViews(ctx, id, record, wave) {
|
|
|
25
23
|
dispatchCtx: dispatchCtx({ collection: collectionName, id })
|
|
26
24
|
});
|
|
27
25
|
} else if (mode === "lazy") {
|
|
28
|
-
if (staleHelpers === null) staleHelpers = await import("./stale-
|
|
26
|
+
if (staleHelpers === null) staleHelpers = await import("./stale-AU74OPUW.js");
|
|
29
27
|
staleHelpers.markMVStale(registry, reg.spec.name);
|
|
30
28
|
}
|
|
31
29
|
}
|
|
@@ -47,7 +45,7 @@ async function dispatchMaterializedViewsOnDelete(ctx, id) {
|
|
|
47
45
|
if (mode === "eager") {
|
|
48
46
|
if (executor === null) {
|
|
49
47
|
;
|
|
50
|
-
({ MaterializedViewExecutor: executor } = await import("./executor-
|
|
48
|
+
({ MaterializedViewExecutor: executor } = await import("./executor-5XJNV5H5.js"));
|
|
51
49
|
}
|
|
52
50
|
const rr = await executor.refresh(reg, {
|
|
53
51
|
getCollection: (name) => source.getCollection(name),
|
|
@@ -60,7 +58,7 @@ async function dispatchMaterializedViewsOnDelete(ctx, id) {
|
|
|
60
58
|
residueDeclined.push(...rr.residueDeclined);
|
|
61
59
|
continue;
|
|
62
60
|
}
|
|
63
|
-
if (staleHelpers === null) staleHelpers = await import("./stale-
|
|
61
|
+
if (staleHelpers === null) staleHelpers = await import("./stale-AU74OPUW.js");
|
|
64
62
|
const inv = await staleHelpers.invalidateMVAtRest(source, reg, mode);
|
|
65
63
|
deleted += inv.deleted;
|
|
66
64
|
residueUndecodable.push(...inv.residueUndecodable.map((rid) => `${reg.outputCollection}:${rid}`));
|
|
@@ -72,4 +70,4 @@ export {
|
|
|
72
70
|
dispatchMaterializedViews,
|
|
73
71
|
dispatchMaterializedViewsOnDelete
|
|
74
72
|
};
|
|
75
|
-
//# sourceMappingURL=dispatch-
|
|
73
|
+
//# sourceMappingURL=dispatch-GGFBWQU7.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-formula/materialized-views/dispatch.ts"],"sourcesContent":["/**\n * Materialized-view dispatch, lifted out of the kernel spine (#842 part b).\n *\n * Reached from `Collection` by dynamic `import()` only — `port-layering`\n * forbids a static spine→`with-*` import, and rejects a type-only one too\n * (it scans import statements, not their erasure). See\n * `with-formula/derivations/dispatch.ts` for the same note.\n *\n * The `Collection` reference below is TYPE-ONLY and points inward\n * (service → kernel), so it is both allowed and erased at build.\n *\n * @internal\n */\n\nimport type { TxContext } from '../../with-commit/tx/transaction.js'\nimport type { Collection } from '../../kernel/collection.js'\nimport type { ledgerAuditHook, WaveContext } from '../../kernel/via/dispatch.js'\nimport type { MaterializedViewExecutor } from './executor.js'\nimport type * as MVStale from './stale.js'\nimport type { MaterializedViewRegistry } from './registry.js'\nimport type { MVQueryContext } from './types.js'\n\n/** The per-write dispatch context the MV executor threads through. */\ninterface DispatchCtx {\n emit: (e: string, p: unknown) => void\n source: { readonly collection: string; readonly id: string }\n audit: ReturnType<typeof ledgerAuditHook>\n}\n\n/** What the MV dispatchers need from the collection that owns them. */\nexport interface MvDispatchCtx {\n readonly materializedViewSource: {\n registry(): MaterializedViewRegistry\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n getCollection(name: string): Collection<any>\n getActiveTxContext(): TxContext | null\n getQueryContext(): MVQueryContext\n }\n /** The collection the delete happened in. */\n readonly collectionName: string\n /** Builds the per-write dispatch context the executor threads through. */\n readonly dispatchCtx: (source: { readonly collection: string; readonly id: string }) => DispatchCtx\n}\n\n/**\n * Fire every MV whose dependency set includes this collection, on write.\n *\n * Skips entirely when the record being written is itself an MV-emitted row\n * (it carries `_materializedFrom`) — a defensive guard against missed cycle\n * detection.\n *\n * @param wave #638 Task 4 — the sync/cutover/restore dispatch wave. An eager\n * MV already refreshed in this wave is skipped, deduped per target on the\n * spec name.\n */\nexport async function dispatchMaterializedViews(\n ctx: MvDispatchCtx,\n id: string,\n record: Record<string, unknown>,\n wave?: WaveContext,\n): Promise<void> {\n const { materializedViewSource: source, collectionName, dispatchCtx } = ctx\n\n if (record && typeof record === 'object' && '_materializedFrom' in record) return\n\n const registry = source.registry()\n // #1139 — replan any query-form strategy parked at vault open because its\n // join named a ref that had not been declared yet. No-op once drained.\n await registry.resolvePendingPlans()\n const mvs = registry.mvsForSource(collectionName)\n if (mvs.length === 0) return\n\n let executor: typeof MaterializedViewExecutor | null = null\n let staleHelpers: typeof MVStale | null = null\n\n for (const reg of mvs) {\n const mode = reg.spec.refresh\n\n if (mode === 'eager') {\n if (wave?.seen(`mv\\0${reg.spec.name}`)) continue\n if (executor === null) {\n ;({ MaterializedViewExecutor: executor } = await import('./executor.js'))\n }\n await executor.refresh(reg, {\n getCollection: (name) => source.getCollection(name),\n getActiveTxContext: () => source.getActiveTxContext(),\n getQueryContext: () => source.getQueryContext(),\n dispatchCtx: dispatchCtx({ collection: collectionName, id }),\n })\n } else if (mode === 'lazy') {\n if (staleHelpers === null) staleHelpers = await import('./stale.js')\n staleHelpers.markMVStale(registry, reg.spec.name)\n }\n // manual: no-op on source-write. `vault.refreshView(name)` is the only\n // path that materializes a manual MV.\n }\n}\n\n/**\n * Mirror of {@link dispatchMaterializedViews} for the delete/forget path. There is\n * no `_materializedFrom` skip — the record is gone — and the `internal` gate\n * at `_doDelete` is the recursion guard.\n *\n * @returns rows TOMBSTONED across every MV sourced here (#638 T6). Eager and\n * lazy/manual `invalidateMVAtRest` purges both contribute (#761 item 1,\n * previously eager-only): lazy persists a stale mark for cold-session\n * recompute, manual serves empty until `refreshView()`.\n * `residueUndecodable` / `residueDeclined` (#776/#785) carry\n * `outputCollection:id` entries whose ownership stamp `invalidateMVAtRest`\n * could not decode, respectively decoded and stamp-matched but declined to\n * erase — surfaced, not erased.\n */\nexport async function dispatchMaterializedViewsOnDelete(\n ctx: MvDispatchCtx,\n id: string,\n): Promise<{ deleted: number; residueUndecodable: string[]; residueDeclined: string[] }> {\n const { materializedViewSource: source, collectionName, dispatchCtx } = ctx\n\n const registry = source.registry()\n await registry.resolvePendingPlans() // #1139 — see the put-path dispatcher\n const mvs = registry.mvsForSource(collectionName)\n const empty = { deleted: 0, residueUndecodable: [], residueDeclined: [] }\n if (mvs.length === 0) return empty\n\n let executor: typeof MaterializedViewExecutor | null = null\n let staleHelpers: typeof MVStale | null = null\n\n let deleted = 0\n const residueUndecodable: string[] = []\n const residueDeclined: string[] = []\n\n for (const reg of mvs) {\n const mode = reg.spec.refresh\n\n if (mode === 'eager') {\n if (executor === null) {\n ;({ MaterializedViewExecutor: executor } = await import('./executor.js'))\n }\n const rr = await executor.refresh(reg, {\n getCollection: (name) => source.getCollection(name),\n getActiveTxContext: () => source.getActiveTxContext(),\n getQueryContext: () => source.getQueryContext(),\n dispatchCtx: dispatchCtx({ collection: collectionName, id }),\n })\n // #782/#785 — the eager leg reports both residue channels.\n deleted += rr.deleted\n residueUndecodable.push(...rr.residueUndecodable)\n residueDeclined.push(...rr.residueDeclined)\n continue\n }\n\n if (staleHelpers === null) staleHelpers = await import('./stale.js')\n const inv = await staleHelpers.invalidateMVAtRest(source, reg, mode)\n deleted += inv.deleted\n residueUndecodable.push(...inv.residueUndecodable.map((rid) => `${reg.outputCollection}:${rid}`))\n residueDeclined.push(...inv.residueDeclined.map((rid) => `${reg.outputCollection}:${rid}`))\n }\n\n return { deleted, residueUndecodable, residueDeclined }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/with-formula/materialized-views/dispatch.ts"],"sourcesContent":["/**\n * Materialized-view dispatch, lifted out of the kernel spine (#842 part b).\n *\n * Reached from `Collection` by dynamic `import()` only — `port-layering`\n * forbids a static spine→`with-*` import, and rejects a type-only one too\n * (it scans import statements, not their erasure). See\n * `with-formula/derivations/dispatch.ts` for the same note.\n *\n * The `Collection` reference below is TYPE-ONLY and points inward\n * (service → kernel), so it is both allowed and erased at build.\n *\n * @internal\n */\n\nimport type { TxContext } from '../../with-commit/tx/transaction.js'\nimport type { Collection } from '../../kernel/collection.js'\nimport type { ledgerAuditHook, WaveContext } from '../../kernel/via/dispatch.js'\nimport type { MaterializedViewExecutor } from './executor.js'\nimport type * as MVStale from './stale.js'\nimport type { MaterializedViewRegistry } from './registry.js'\nimport type { MVQueryContext } from './types.js'\n\n/** The per-write dispatch context the MV executor threads through. */\ninterface DispatchCtx {\n emit: (e: string, p: unknown) => void\n source: { readonly collection: string; readonly id: string }\n audit: ReturnType<typeof ledgerAuditHook>\n}\n\n/** What the MV dispatchers need from the collection that owns them. */\nexport interface MvDispatchCtx {\n readonly materializedViewSource: {\n registry(): MaterializedViewRegistry\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n getCollection(name: string): Collection<any>\n getActiveTxContext(): TxContext | null\n getQueryContext(): MVQueryContext\n }\n /** The collection the delete happened in. */\n readonly collectionName: string\n /** Builds the per-write dispatch context the executor threads through. */\n readonly dispatchCtx: (source: { readonly collection: string; readonly id: string }) => DispatchCtx\n}\n\n/**\n * Fire every MV whose dependency set includes this collection, on write.\n *\n * Skips entirely when the record being written is itself an MV-emitted row\n * (it carries `_materializedFrom`) — a defensive guard against missed cycle\n * detection.\n *\n * @param wave #638 Task 4 — the sync/cutover/restore dispatch wave. An eager\n * MV already refreshed in this wave is skipped, deduped per target on the\n * spec name.\n */\nexport async function dispatchMaterializedViews(\n ctx: MvDispatchCtx,\n id: string,\n record: Record<string, unknown>,\n wave?: WaveContext,\n): Promise<void> {\n const { materializedViewSource: source, collectionName, dispatchCtx } = ctx\n\n if (record && typeof record === 'object' && '_materializedFrom' in record) return\n\n const registry = source.registry()\n // #1139 — replan any query-form strategy parked at vault open because its\n // join named a ref that had not been declared yet. No-op once drained.\n await registry.resolvePendingPlans()\n const mvs = registry.mvsForSource(collectionName)\n if (mvs.length === 0) return\n\n let executor: typeof MaterializedViewExecutor | null = null\n let staleHelpers: typeof MVStale | null = null\n\n for (const reg of mvs) {\n const mode = reg.spec.refresh\n\n if (mode === 'eager') {\n if (wave?.seen(`mv\\0${reg.spec.name}`)) continue\n if (executor === null) {\n ;({ MaterializedViewExecutor: executor } = await import('./executor.js'))\n }\n await executor.refresh(reg, {\n getCollection: (name) => source.getCollection(name),\n getActiveTxContext: () => source.getActiveTxContext(),\n getQueryContext: () => source.getQueryContext(),\n dispatchCtx: dispatchCtx({ collection: collectionName, id }),\n })\n } else if (mode === 'lazy') {\n if (staleHelpers === null) staleHelpers = await import('./stale.js')\n staleHelpers.markMVStale(registry, reg.spec.name)\n }\n // manual: no-op on source-write. `vault.refreshView(name)` is the only\n // path that materializes a manual MV.\n }\n}\n\n/**\n * Mirror of {@link dispatchMaterializedViews} for the delete/forget path. There is\n * no `_materializedFrom` skip — the record is gone — and the `internal` gate\n * at `_doDelete` is the recursion guard.\n *\n * @returns rows TOMBSTONED across every MV sourced here (#638 T6). Eager and\n * lazy/manual `invalidateMVAtRest` purges both contribute (#761 item 1,\n * previously eager-only): lazy persists a stale mark for cold-session\n * recompute, manual serves empty until `refreshView()`.\n * `residueUndecodable` / `residueDeclined` (#776/#785) carry\n * `outputCollection:id` entries whose ownership stamp `invalidateMVAtRest`\n * could not decode, respectively decoded and stamp-matched but declined to\n * erase — surfaced, not erased.\n */\nexport async function dispatchMaterializedViewsOnDelete(\n ctx: MvDispatchCtx,\n id: string,\n): Promise<{ deleted: number; residueUndecodable: string[]; residueDeclined: string[] }> {\n const { materializedViewSource: source, collectionName, dispatchCtx } = ctx\n\n const registry = source.registry()\n await registry.resolvePendingPlans() // #1139 — see the put-path dispatcher\n const mvs = registry.mvsForSource(collectionName)\n const empty = { deleted: 0, residueUndecodable: [], residueDeclined: [] }\n if (mvs.length === 0) return empty\n\n let executor: typeof MaterializedViewExecutor | null = null\n let staleHelpers: typeof MVStale | null = null\n\n let deleted = 0\n const residueUndecodable: string[] = []\n const residueDeclined: string[] = []\n\n for (const reg of mvs) {\n const mode = reg.spec.refresh\n\n if (mode === 'eager') {\n if (executor === null) {\n ;({ MaterializedViewExecutor: executor } = await import('./executor.js'))\n }\n const rr = await executor.refresh(reg, {\n getCollection: (name) => source.getCollection(name),\n getActiveTxContext: () => source.getActiveTxContext(),\n getQueryContext: () => source.getQueryContext(),\n dispatchCtx: dispatchCtx({ collection: collectionName, id }),\n })\n // #782/#785 — the eager leg reports both residue channels.\n deleted += rr.deleted\n residueUndecodable.push(...rr.residueUndecodable)\n residueDeclined.push(...rr.residueDeclined)\n continue\n }\n\n if (staleHelpers === null) staleHelpers = await import('./stale.js')\n const inv = await staleHelpers.invalidateMVAtRest(source, reg, mode)\n deleted += inv.deleted\n residueUndecodable.push(...inv.residueUndecodable.map((rid) => `${reg.outputCollection}:${rid}`))\n residueDeclined.push(...inv.residueDeclined.map((rid) => `${reg.outputCollection}:${rid}`))\n }\n\n return { deleted, residueUndecodable, residueDeclined }\n}\n"],"mappings":";AAuDA,eAAsB,0BACpB,KACA,IACA,QACA,MACe;AACf,QAAM,EAAE,wBAAwB,QAAQ,gBAAgB,YAAY,IAAI;AAExE,MAAI,UAAU,OAAO,WAAW,YAAY,uBAAuB,OAAQ;AAE3E,QAAM,WAAW,OAAO,SAAS;AAGjC,QAAM,SAAS,oBAAoB;AACnC,QAAM,MAAM,SAAS,aAAa,cAAc;AAChD,MAAI,IAAI,WAAW,EAAG;AAEtB,MAAI,WAAmD;AACvD,MAAI,eAAsC;AAE1C,aAAW,OAAO,KAAK;AACrB,UAAM,OAAO,IAAI,KAAK;AAEtB,QAAI,SAAS,SAAS;AACpB,UAAI,MAAM,KAAK,OAAO,IAAI,KAAK,IAAI,EAAE,EAAG;AACxC,UAAI,aAAa,MAAM;AACrB;AAAC,SAAC,EAAE,0BAA0B,SAAS,IAAI,MAAM,OAAO,wBAAe;AAAA,MACzE;AACA,YAAM,SAAS,QAAQ,KAAK;AAAA,QAC1B,eAAe,CAAC,SAAS,OAAO,cAAc,IAAI;AAAA,QAClD,oBAAoB,MAAM,OAAO,mBAAmB;AAAA,QACpD,iBAAiB,MAAM,OAAO,gBAAgB;AAAA,QAC9C,aAAa,YAAY,EAAE,YAAY,gBAAgB,GAAG,CAAC;AAAA,MAC7D,CAAC;AAAA,IACH,WAAW,SAAS,QAAQ;AAC1B,UAAI,iBAAiB,KAAM,gBAAe,MAAM,OAAO,qBAAY;AACnE,mBAAa,YAAY,UAAU,IAAI,KAAK,IAAI;AAAA,IAClD;AAAA,EAGF;AACF;AAgBA,eAAsB,kCACpB,KACA,IACuF;AACvF,QAAM,EAAE,wBAAwB,QAAQ,gBAAgB,YAAY,IAAI;AAExE,QAAM,WAAW,OAAO,SAAS;AACjC,QAAM,SAAS,oBAAoB;AACnC,QAAM,MAAM,SAAS,aAAa,cAAc;AAChD,QAAM,QAAQ,EAAE,SAAS,GAAG,oBAAoB,CAAC,GAAG,iBAAiB,CAAC,EAAE;AACxE,MAAI,IAAI,WAAW,EAAG,QAAO;AAE7B,MAAI,WAAmD;AACvD,MAAI,eAAsC;AAE1C,MAAI,UAAU;AACd,QAAM,qBAA+B,CAAC;AACtC,QAAM,kBAA4B,CAAC;AAEnC,aAAW,OAAO,KAAK;AACrB,UAAM,OAAO,IAAI,KAAK;AAEtB,QAAI,SAAS,SAAS;AACpB,UAAI,aAAa,MAAM;AACrB;AAAC,SAAC,EAAE,0BAA0B,SAAS,IAAI,MAAM,OAAO,wBAAe;AAAA,MACzE;AACA,YAAM,KAAK,MAAM,SAAS,QAAQ,KAAK;AAAA,QACrC,eAAe,CAAC,SAAS,OAAO,cAAc,IAAI;AAAA,QAClD,oBAAoB,MAAM,OAAO,mBAAmB;AAAA,QACpD,iBAAiB,MAAM,OAAO,gBAAgB;AAAA,QAC9C,aAAa,YAAY,EAAE,YAAY,gBAAgB,GAAG,CAAC;AAAA,MAC7D,CAAC;AAED,iBAAW,GAAG;AACd,yBAAmB,KAAK,GAAG,GAAG,kBAAkB;AAChD,sBAAgB,KAAK,GAAG,GAAG,eAAe;AAC1C;AAAA,IACF;AAEA,QAAI,iBAAiB,KAAM,gBAAe,MAAM,OAAO,qBAAY;AACnE,UAAM,MAAM,MAAM,aAAa,mBAAmB,QAAQ,KAAK,IAAI;AACnE,eAAW,IAAI;AACf,uBAAmB,KAAK,GAAG,IAAI,mBAAmB,IAAI,CAAC,QAAQ,GAAG,IAAI,gBAAgB,IAAI,GAAG,EAAE,CAAC;AAChG,oBAAgB,KAAK,GAAG,IAAI,gBAAgB,IAAI,CAAC,QAAQ,GAAG,IAAI,gBAAgB,IAAI,GAAG,EAAE,CAAC;AAAA,EAC5F;AAEA,SAAO,EAAE,SAAS,oBAAoB,gBAAgB;AACxD;","names":[]}
|
|
@@ -88,7 +88,6 @@ import {
|
|
|
88
88
|
} from "./chunk-SL3JAAKN.js";
|
|
89
89
|
import "./chunk-N4CHCXDL.js";
|
|
90
90
|
import "./chunk-7O3Y4PPD.js";
|
|
91
|
-
import "./chunk-PZ5AY32C.js";
|
|
92
91
|
export {
|
|
93
92
|
RecordCodec,
|
|
94
93
|
SEALED_CEK_NS,
|
|
@@ -166,4 +165,4 @@ export {
|
|
|
166
165
|
wrapKey,
|
|
167
166
|
writeEnvelopeBody
|
|
168
167
|
};
|
|
169
|
-
//# sourceMappingURL=enclave-
|
|
168
|
+
//# sourceMappingURL=enclave-VM2YQONI.js.map
|
|
@@ -23,8 +23,7 @@ import "./chunk-LP7BEXCT.js";
|
|
|
23
23
|
import "./chunk-SL3JAAKN.js";
|
|
24
24
|
import "./chunk-N4CHCXDL.js";
|
|
25
25
|
import "./chunk-7O3Y4PPD.js";
|
|
26
|
-
import "./chunk-PZ5AY32C.js";
|
|
27
26
|
export {
|
|
28
27
|
MaterializedViewExecutor
|
|
29
28
|
};
|
|
30
|
-
//# sourceMappingURL=executor-
|
|
29
|
+
//# sourceMappingURL=executor-5XJNV5H5.js.map
|
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
buildAccessibleBundle,
|
|
3
3
|
exportAccessibleData,
|
|
4
4
|
resolveAccessibleCollections
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-EKZ6VZYF.js";
|
|
6
|
+
import "./chunk-NLOLF6LD.js";
|
|
7
7
|
import "./chunk-MLO75SFJ.js";
|
|
8
8
|
import "./chunk-CIGAQBJR.js";
|
|
9
9
|
import "./chunk-4RUFYGWG.js";
|
|
@@ -14,10 +14,9 @@ import "./chunk-LP7BEXCT.js";
|
|
|
14
14
|
import "./chunk-SL3JAAKN.js";
|
|
15
15
|
import "./chunk-N4CHCXDL.js";
|
|
16
16
|
import "./chunk-7O3Y4PPD.js";
|
|
17
|
-
import "./chunk-PZ5AY32C.js";
|
|
18
17
|
export {
|
|
19
18
|
buildAccessibleBundle,
|
|
20
19
|
exportAccessibleData,
|
|
21
20
|
resolveAccessibleCollections
|
|
22
21
|
};
|
|
23
|
-
//# sourceMappingURL=export-accessible-
|
|
22
|
+
//# sourceMappingURL=export-accessible-2LOBM67T.js.map
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
reKeyLedger,
|
|
7
7
|
reKeySchemas,
|
|
8
8
|
sealDeks
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-3LK2XZCW.js";
|
|
10
10
|
import "./chunk-WWNYSV7S.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-NLOLF6LD.js";
|
|
12
12
|
import "./chunk-MLO75SFJ.js";
|
|
13
13
|
import "./chunk-ZG4XLOKB.js";
|
|
14
14
|
import "./chunk-UGTBXF5U.js";
|
|
@@ -25,7 +25,6 @@ import "./chunk-LP7BEXCT.js";
|
|
|
25
25
|
import "./chunk-SL3JAAKN.js";
|
|
26
26
|
import "./chunk-N4CHCXDL.js";
|
|
27
27
|
import "./chunk-7O3Y4PPD.js";
|
|
28
|
-
import "./chunk-PZ5AY32C.js";
|
|
29
28
|
export {
|
|
30
29
|
extractPartition,
|
|
31
30
|
extractPartitionCore,
|
|
@@ -35,4 +34,4 @@ export {
|
|
|
35
34
|
reKeySchemas,
|
|
36
35
|
sealDeks
|
|
37
36
|
};
|
|
38
|
-
//# sourceMappingURL=extract-partition-
|
|
37
|
+
//# sourceMappingURL=extract-partition-AZGSEMIF.js.map
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from "./chunk-SL3JAAKN.js";
|
|
15
15
|
import "./chunk-N4CHCXDL.js";
|
|
16
16
|
import "./chunk-7O3Y4PPD.js";
|
|
17
|
-
import "./chunk-PZ5AY32C.js";
|
|
18
17
|
|
|
19
18
|
// src/with-formula/derivations/fanout-sidecar.ts
|
|
20
19
|
var FANOUT_DEK_COLLECTION = "_meta";
|
|
@@ -62,4 +61,4 @@ export {
|
|
|
62
61
|
loadFanoutSidecar,
|
|
63
62
|
saveFanoutSidecar
|
|
64
63
|
};
|
|
65
|
-
//# sourceMappingURL=fanout-sidecar-
|
|
64
|
+
//# sourceMappingURL=fanout-sidecar-HJIVUPTD.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-formula/derivations/fanout-sidecar.ts"],"sourcesContent":["/**\n * Per-source-row fanout sidecar for `shape: 'array'` derivations.\n *\n * Each `(sourceCollection, sourceId, outputKey)` triple gets its own\n * envelope at:\n *\n * _meta/derivations-fanout/<sourceCollection>/<sourceId>/<outputKey>\n *\n * The envelope records the last-emitted derived row ids so the\n * dispatcher can compute the diff on every source-row update in O(1):\n * read prior keys, compute `toDelete = prev \\ new`, write new, persist\n * back.\n *\n * The body is encrypted (AES-GCM under the `_meta` collection DEK) when\n * the vault is encrypted — the `keys[]` are derived-row ids produced by a\n * user-supplied key extractor and can be content-bearing (SKU, tag, email),\n * so a ciphertext-only store must not read them, the derivation graph, or\n * `emittedAt`. Back-compat: sidecars written before this fix are plaintext\n * (`_iv === ''`); `loadFanoutSidecar` dual-reads them.\n *\n * @module\n */\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport { buildRecordAad, buildRecordEnvelope, encrypt, openEnvelopeJson, type EnclaveKey } from '../../kernel/enclave/index.js'\n\ntype GetDEK = (collectionName: string) => Promise<EnclaveKey>\n\n/** The `_meta` collection name whose DEK encrypts the sidecar body. */\nconst FANOUT_DEK_COLLECTION = '_meta'\n\n/** Magic-prefixed JSON payload at `_meta/<recordId>`. */\nexport interface FanoutSidecar {\n readonly _noydb_fanout: 1\n /** Source collection name. */\n readonly source: string\n /** Source record id. */\n readonly sourceId: string\n /** Strategy output key (the key in `strategy.outputs`). */\n readonly outputKey: string\n /** Output collection name (audit / forensics). */\n readonly outputCollection: string\n /** Derived-row ids last emitted for this (source, output) pair. */\n readonly keys: ReadonlyArray<string>\n /** ISO timestamp of last dispatch. */\n readonly emittedAt: string\n}\n\n/**\n * Build the canonical `_meta` record id for a fanout sidecar.\n *\n * The full path inside the store is `_meta/<this-string>`. We pack the\n * full triple into the id so a single `_meta` collection holds all\n * sidecars (collection-per-source would proliferate names; the\n * existing `_meta` flat namespace is cheaper).\n */\nfunction recordId(source: string, sourceId: string, outputKey: string): string {\n // Use `/` as a separator. None of the components is supposed to\n // contain it (collection names + output keys are bare identifiers;\n // source ids are typically ULIDs / UUIDs / app-chosen strings\n // without slashes). If a future source carries `/` in its id, we'd\n // need an escape; deferred until that's a real case.\n return `derivations-fanout/${source}/${sourceId}/${outputKey}`\n}\n\n/**\n * Read the sidecar; returns `undefined` only when it's legitimately absent\n * (no envelope at that id) — the correct signal for callers to skip\n * orphan-row reconciliation because there's nothing to reconcile against.\n *\n * A PRESENT envelope that fails to decrypt or parse is a data-integrity\n * problem, not an absence, and is deliberately NOT caught here: swallowing\n * it to `undefined` would look identical to \"no sidecar\" and make callers\n * (the array-derivation dispatch in `vault.ts`/`collection.ts`) silently\n * skip deleting stale derived rows on a shrink. Let `openEnvelopeJson`'s\n * `DecryptionError`/`TamperedError` and `JSON.parse` failures propagate,\n * matching the no-catch convention of sibling decrypt call-sites (e.g.\n * `with-audit/consent/consent.ts`'s `decryptEntry`).\n *\n * Dual-reads for back-compat: an envelope with `_iv === ''` is a legacy\n * plaintext sidecar (parse `_data` directly); otherwise the body was\n * encrypted under the `_meta` DEK and is decrypted first.\n */\nexport async function loadFanoutSidecar(\n store: NoydbStore,\n vault: string,\n source: string,\n sourceId: string,\n outputKey: string,\n getDEK: GetDEK,\n encrypted: boolean,\n): Promise<FanoutSidecar | undefined> {\n const envelope = await store.get(vault, '_meta', recordId(source, sourceId, outputKey))\n if (!envelope) return undefined\n // Legacy plaintext (`_iv === ''`) reads directly; encrypted bodies decrypt.\n const json = (!encrypted || envelope._iv === '')\n ? envelope._data\n : await openEnvelopeJson({ collection: '_meta', id: recordId(source, sourceId, outputKey) }, envelope, await getDEK(FANOUT_DEK_COLLECTION))\n const parsed = JSON.parse(json) as FanoutSidecar\n if (parsed._noydb_fanout !== 1) return undefined\n if (!Array.isArray(parsed.keys)) return undefined\n return parsed\n}\n\n/**\n * Persist (insert/replace) the sidecar with a fresh key set. The body is\n * encrypted under the `_meta` DEK when the vault is encrypted (the `keys[]`\n * can be content-bearing); plaintext only in debug/unencrypted vaults.\n */\nexport async function saveFanoutSidecar(\n store: NoydbStore,\n vault: string,\n payload: {\n readonly source: string\n readonly sourceId: string\n readonly outputKey: string\n readonly outputCollection: string\n readonly keys: ReadonlyArray<string>\n },\n getDEK: GetDEK,\n encrypted: boolean,\n): Promise<void> {\n const doc: FanoutSidecar = {\n _noydb_fanout: 1,\n source: payload.source,\n sourceId: payload.sourceId,\n outputKey: payload.outputKey,\n outputCollection: payload.outputCollection,\n keys: payload.keys,\n emittedAt: new Date().toISOString(),\n }\n const id = recordId(payload.source, payload.sourceId, payload.outputKey)\n const prior = await store.get(vault, '_meta', id)\n const json = JSON.stringify(doc)\n const version = (prior?._v ?? 0) + 1\n const identity = { collection: '_meta', id, version }\n let envelope: EncryptedEnvelope\n if (!encrypted) {\n envelope = buildRecordEnvelope(identity, { iv: '', data: json })\n } else {\n const { iv, data } = await encrypt(json, await getDEK(FANOUT_DEK_COLLECTION), buildRecordAad(identity))\n envelope = buildRecordEnvelope(identity, { iv, data })\n }\n await store.put(vault, '_meta', id, envelope)\n}\n\n/** Delete the sidecar (used on source-row delete cascade). */\nexport async function deleteFanoutSidecar(\n store: NoydbStore,\n vault: string,\n source: string,\n sourceId: string,\n outputKey: string,\n): Promise<void> {\n await store.delete(vault, '_meta', recordId(source, sourceId, outputKey))\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/with-formula/derivations/fanout-sidecar.ts"],"sourcesContent":["/**\n * Per-source-row fanout sidecar for `shape: 'array'` derivations.\n *\n * Each `(sourceCollection, sourceId, outputKey)` triple gets its own\n * envelope at:\n *\n * _meta/derivations-fanout/<sourceCollection>/<sourceId>/<outputKey>\n *\n * The envelope records the last-emitted derived row ids so the\n * dispatcher can compute the diff on every source-row update in O(1):\n * read prior keys, compute `toDelete = prev \\ new`, write new, persist\n * back.\n *\n * The body is encrypted (AES-GCM under the `_meta` collection DEK) when\n * the vault is encrypted — the `keys[]` are derived-row ids produced by a\n * user-supplied key extractor and can be content-bearing (SKU, tag, email),\n * so a ciphertext-only store must not read them, the derivation graph, or\n * `emittedAt`. Back-compat: sidecars written before this fix are plaintext\n * (`_iv === ''`); `loadFanoutSidecar` dual-reads them.\n *\n * @module\n */\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport { buildRecordAad, buildRecordEnvelope, encrypt, openEnvelopeJson, type EnclaveKey } from '../../kernel/enclave/index.js'\n\ntype GetDEK = (collectionName: string) => Promise<EnclaveKey>\n\n/** The `_meta` collection name whose DEK encrypts the sidecar body. */\nconst FANOUT_DEK_COLLECTION = '_meta'\n\n/** Magic-prefixed JSON payload at `_meta/<recordId>`. */\nexport interface FanoutSidecar {\n readonly _noydb_fanout: 1\n /** Source collection name. */\n readonly source: string\n /** Source record id. */\n readonly sourceId: string\n /** Strategy output key (the key in `strategy.outputs`). */\n readonly outputKey: string\n /** Output collection name (audit / forensics). */\n readonly outputCollection: string\n /** Derived-row ids last emitted for this (source, output) pair. */\n readonly keys: ReadonlyArray<string>\n /** ISO timestamp of last dispatch. */\n readonly emittedAt: string\n}\n\n/**\n * Build the canonical `_meta` record id for a fanout sidecar.\n *\n * The full path inside the store is `_meta/<this-string>`. We pack the\n * full triple into the id so a single `_meta` collection holds all\n * sidecars (collection-per-source would proliferate names; the\n * existing `_meta` flat namespace is cheaper).\n */\nfunction recordId(source: string, sourceId: string, outputKey: string): string {\n // Use `/` as a separator. None of the components is supposed to\n // contain it (collection names + output keys are bare identifiers;\n // source ids are typically ULIDs / UUIDs / app-chosen strings\n // without slashes). If a future source carries `/` in its id, we'd\n // need an escape; deferred until that's a real case.\n return `derivations-fanout/${source}/${sourceId}/${outputKey}`\n}\n\n/**\n * Read the sidecar; returns `undefined` only when it's legitimately absent\n * (no envelope at that id) — the correct signal for callers to skip\n * orphan-row reconciliation because there's nothing to reconcile against.\n *\n * A PRESENT envelope that fails to decrypt or parse is a data-integrity\n * problem, not an absence, and is deliberately NOT caught here: swallowing\n * it to `undefined` would look identical to \"no sidecar\" and make callers\n * (the array-derivation dispatch in `vault.ts`/`collection.ts`) silently\n * skip deleting stale derived rows on a shrink. Let `openEnvelopeJson`'s\n * `DecryptionError`/`TamperedError` and `JSON.parse` failures propagate,\n * matching the no-catch convention of sibling decrypt call-sites (e.g.\n * `with-audit/consent/consent.ts`'s `decryptEntry`).\n *\n * Dual-reads for back-compat: an envelope with `_iv === ''` is a legacy\n * plaintext sidecar (parse `_data` directly); otherwise the body was\n * encrypted under the `_meta` DEK and is decrypted first.\n */\nexport async function loadFanoutSidecar(\n store: NoydbStore,\n vault: string,\n source: string,\n sourceId: string,\n outputKey: string,\n getDEK: GetDEK,\n encrypted: boolean,\n): Promise<FanoutSidecar | undefined> {\n const envelope = await store.get(vault, '_meta', recordId(source, sourceId, outputKey))\n if (!envelope) return undefined\n // Legacy plaintext (`_iv === ''`) reads directly; encrypted bodies decrypt.\n const json = (!encrypted || envelope._iv === '')\n ? envelope._data\n : await openEnvelopeJson({ collection: '_meta', id: recordId(source, sourceId, outputKey) }, envelope, await getDEK(FANOUT_DEK_COLLECTION))\n const parsed = JSON.parse(json) as FanoutSidecar\n if (parsed._noydb_fanout !== 1) return undefined\n if (!Array.isArray(parsed.keys)) return undefined\n return parsed\n}\n\n/**\n * Persist (insert/replace) the sidecar with a fresh key set. The body is\n * encrypted under the `_meta` DEK when the vault is encrypted (the `keys[]`\n * can be content-bearing); plaintext only in debug/unencrypted vaults.\n */\nexport async function saveFanoutSidecar(\n store: NoydbStore,\n vault: string,\n payload: {\n readonly source: string\n readonly sourceId: string\n readonly outputKey: string\n readonly outputCollection: string\n readonly keys: ReadonlyArray<string>\n },\n getDEK: GetDEK,\n encrypted: boolean,\n): Promise<void> {\n const doc: FanoutSidecar = {\n _noydb_fanout: 1,\n source: payload.source,\n sourceId: payload.sourceId,\n outputKey: payload.outputKey,\n outputCollection: payload.outputCollection,\n keys: payload.keys,\n emittedAt: new Date().toISOString(),\n }\n const id = recordId(payload.source, payload.sourceId, payload.outputKey)\n const prior = await store.get(vault, '_meta', id)\n const json = JSON.stringify(doc)\n const version = (prior?._v ?? 0) + 1\n const identity = { collection: '_meta', id, version }\n let envelope: EncryptedEnvelope\n if (!encrypted) {\n envelope = buildRecordEnvelope(identity, { iv: '', data: json })\n } else {\n const { iv, data } = await encrypt(json, await getDEK(FANOUT_DEK_COLLECTION), buildRecordAad(identity))\n envelope = buildRecordEnvelope(identity, { iv, data })\n }\n await store.put(vault, '_meta', id, envelope)\n}\n\n/** Delete the sidecar (used on source-row delete cascade). */\nexport async function deleteFanoutSidecar(\n store: NoydbStore,\n vault: string,\n source: string,\n sourceId: string,\n outputKey: string,\n): Promise<void> {\n await store.delete(vault, '_meta', recordId(source, sourceId, outputKey))\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA4BA,IAAM,wBAAwB;AA2B9B,SAAS,SAAS,QAAgB,UAAkB,WAA2B;AAM7E,SAAO,sBAAsB,MAAM,IAAI,QAAQ,IAAI,SAAS;AAC9D;AAoBA,eAAsB,kBACpB,OACA,OACA,QACA,UACA,WACA,QACA,WACoC;AACpC,QAAM,WAAW,MAAM,MAAM,IAAI,OAAO,SAAS,SAAS,QAAQ,UAAU,SAAS,CAAC;AACtF,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,OAAQ,CAAC,aAAa,SAAS,QAAQ,KACzC,SAAS,QACT,MAAM,iBAAiB,EAAE,YAAY,SAAS,IAAI,SAAS,QAAQ,UAAU,SAAS,EAAE,GAAG,UAAU,MAAM,OAAO,qBAAqB,CAAC;AAC5I,QAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,MAAI,OAAO,kBAAkB,EAAG,QAAO;AACvC,MAAI,CAAC,MAAM,QAAQ,OAAO,IAAI,EAAG,QAAO;AACxC,SAAO;AACT;AAOA,eAAsB,kBACpB,OACA,OACA,SAOA,QACA,WACe;AACf,QAAM,MAAqB;AAAA,IACzB,eAAe;AAAA,IACf,QAAQ,QAAQ;AAAA,IAChB,UAAU,QAAQ;AAAA,IAClB,WAAW,QAAQ;AAAA,IACnB,kBAAkB,QAAQ;AAAA,IAC1B,MAAM,QAAQ;AAAA,IACd,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,EACpC;AACA,QAAM,KAAK,SAAS,QAAQ,QAAQ,QAAQ,UAAU,QAAQ,SAAS;AACvE,QAAM,QAAQ,MAAM,MAAM,IAAI,OAAO,SAAS,EAAE;AAChD,QAAM,OAAO,KAAK,UAAU,GAAG;AAC/B,QAAM,WAAW,OAAO,MAAM,KAAK;AACnC,QAAM,WAAW,EAAE,YAAY,SAAS,IAAI,QAAQ;AACpD,MAAI;AACJ,MAAI,CAAC,WAAW;AACd,eAAW,oBAAoB,UAAU,EAAE,IAAI,IAAI,MAAM,KAAK,CAAC;AAAA,EACjE,OAAO;AACL,UAAM,EAAE,IAAI,KAAK,IAAI,MAAM,QAAQ,MAAM,MAAM,OAAO,qBAAqB,GAAG,eAAe,QAAQ,CAAC;AACtG,eAAW,oBAAoB,UAAU,EAAE,IAAI,KAAK,CAAC;AAAA,EACvD;AACA,QAAM,MAAM,IAAI,OAAO,SAAS,IAAI,QAAQ;AAC9C;AAGA,eAAsB,oBACpB,OACA,OACA,QACA,UACA,WACe;AACf,QAAM,MAAM,OAAO,OAAO,SAAS,SAAS,QAAQ,UAAU,SAAS,CAAC;AAC1E;","names":[]}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import "./chunk-PZ5AY32C.js";
|
|
2
|
-
|
|
3
1
|
// src/with-shape/schema-update/fence-watcher.ts
|
|
4
2
|
var FenceWatcher = class {
|
|
5
3
|
#coordination;
|
|
@@ -66,4 +64,4 @@ var FenceWatcher = class {
|
|
|
66
64
|
export {
|
|
67
65
|
FenceWatcher
|
|
68
66
|
};
|
|
69
|
-
//# sourceMappingURL=fence-watcher-
|
|
67
|
+
//# sourceMappingURL=fence-watcher-U2PW6JU5.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-shape/schema-update/fence-watcher.ts"],"sourcesContent":["/**\n * Per-client schema-fence watcher. Watches the fence via the injected\n * {@link NoydbMesh}; on `draining` it drains in-flight writes\n * and reports presence (its ack); emits a same-instance signal on every\n * state transition (for UI). Driven by an interval in production and by\n * explicit `check()`/`beat()` in tests.\n *\n * The transport is the coordination port: the default\n * {@link StoreMesh} maps `reportPresence` →\n * `_meta/schema-fence:client:<id>` and `readFence` → `_meta/schema-fence`,\n * so behavior is byte-for-byte the same; `by-tabs` / `by-peer` swap in\n * real-time push transports.\n */\nimport { type FenceState } from './fence.js'\nimport type { NoydbMesh } from '../../port/by/index.js'\n\nexport interface FenceWatcherEvent {\n readonly currentSchemaVersion: number\n readonly fenceState: FenceState\n}\n\nexport class FenceWatcher {\n readonly #coordination: NoydbMesh\n readonly #vault: string\n readonly #writerId: string\n readonly #sessionId: string\n readonly #onFlush: () => Promise<void>\n readonly #now: () => number\n readonly #emit: (e: FenceWatcherEvent) => void\n #lastState: FenceState | null = null\n #quiescedAtVersion: number | null = null\n #timer: ReturnType<typeof setInterval> | undefined\n\n constructor(opts: {\n coordination: NoydbMesh\n vault: string\n clientId: string\n sessionId?: string\n onFlush: () => Promise<void>\n now?: () => number\n emit?: (e: FenceWatcherEvent) => void\n }) {\n this.#coordination = opts.coordination\n this.#vault = opts.vault\n this.#writerId = opts.clientId\n this.#sessionId = opts.sessionId ?? opts.clientId\n this.#onFlush = opts.onFlush\n this.#now = opts.now ?? (() => Date.now())\n this.#emit = opts.emit ?? (() => {})\n }\n\n /** Publish liveness (and the current ack) without changing quiesce state. */\n async beat(): Promise<void> {\n await this.#coordination.reportPresence(this.#vault, {\n writerId: this.#writerId,\n sessionId: this.#sessionId,\n lastSeen: this.#now(),\n quiescedAtVersion: this.#quiescedAtVersion,\n })\n }\n\n /** Poll the fence; quiesce on draining; emit on transitions. */\n async check(): Promise<void> {\n const fence = await this.#coordination.readFence(this.#vault)\n if (fence.fenceState !== this.#lastState) {\n this.#lastState = fence.fenceState\n this.#emit({ currentSchemaVersion: fence.currentSchemaVersion, fenceState: fence.fenceState })\n }\n if (fence.fenceState === 'draining' && this.#quiescedAtVersion !== fence.currentSchemaVersion) {\n await this.#onFlush()\n this.#quiescedAtVersion = fence.currentSchemaVersion\n await this.beat()\n }\n if (fence.fenceState === 'normal') {\n this.#quiescedAtVersion = null\n }\n }\n\n start(intervalMs: number): void {\n if (this.#timer) return\n this.#timer = setInterval(() => { void this.beat(); void this.check() }, intervalMs)\n const timer = this.#timer as unknown as { unref?: () => void }\n if (typeof timer.unref === 'function') timer.unref() // don't keep the process alive\n }\n\n stop(): void {\n if (this.#timer) { clearInterval(this.#timer); this.#timer = undefined }\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/with-shape/schema-update/fence-watcher.ts"],"sourcesContent":["/**\n * Per-client schema-fence watcher. Watches the fence via the injected\n * {@link NoydbMesh}; on `draining` it drains in-flight writes\n * and reports presence (its ack); emits a same-instance signal on every\n * state transition (for UI). Driven by an interval in production and by\n * explicit `check()`/`beat()` in tests.\n *\n * The transport is the coordination port: the default\n * {@link StoreMesh} maps `reportPresence` →\n * `_meta/schema-fence:client:<id>` and `readFence` → `_meta/schema-fence`,\n * so behavior is byte-for-byte the same; `by-tabs` / `by-peer` swap in\n * real-time push transports.\n */\nimport { type FenceState } from './fence.js'\nimport type { NoydbMesh } from '../../port/by/index.js'\n\nexport interface FenceWatcherEvent {\n readonly currentSchemaVersion: number\n readonly fenceState: FenceState\n}\n\nexport class FenceWatcher {\n readonly #coordination: NoydbMesh\n readonly #vault: string\n readonly #writerId: string\n readonly #sessionId: string\n readonly #onFlush: () => Promise<void>\n readonly #now: () => number\n readonly #emit: (e: FenceWatcherEvent) => void\n #lastState: FenceState | null = null\n #quiescedAtVersion: number | null = null\n #timer: ReturnType<typeof setInterval> | undefined\n\n constructor(opts: {\n coordination: NoydbMesh\n vault: string\n clientId: string\n sessionId?: string\n onFlush: () => Promise<void>\n now?: () => number\n emit?: (e: FenceWatcherEvent) => void\n }) {\n this.#coordination = opts.coordination\n this.#vault = opts.vault\n this.#writerId = opts.clientId\n this.#sessionId = opts.sessionId ?? opts.clientId\n this.#onFlush = opts.onFlush\n this.#now = opts.now ?? (() => Date.now())\n this.#emit = opts.emit ?? (() => {})\n }\n\n /** Publish liveness (and the current ack) without changing quiesce state. */\n async beat(): Promise<void> {\n await this.#coordination.reportPresence(this.#vault, {\n writerId: this.#writerId,\n sessionId: this.#sessionId,\n lastSeen: this.#now(),\n quiescedAtVersion: this.#quiescedAtVersion,\n })\n }\n\n /** Poll the fence; quiesce on draining; emit on transitions. */\n async check(): Promise<void> {\n const fence = await this.#coordination.readFence(this.#vault)\n if (fence.fenceState !== this.#lastState) {\n this.#lastState = fence.fenceState\n this.#emit({ currentSchemaVersion: fence.currentSchemaVersion, fenceState: fence.fenceState })\n }\n if (fence.fenceState === 'draining' && this.#quiescedAtVersion !== fence.currentSchemaVersion) {\n await this.#onFlush()\n this.#quiescedAtVersion = fence.currentSchemaVersion\n await this.beat()\n }\n if (fence.fenceState === 'normal') {\n this.#quiescedAtVersion = null\n }\n }\n\n start(intervalMs: number): void {\n if (this.#timer) return\n this.#timer = setInterval(() => { void this.beat(); void this.check() }, intervalMs)\n const timer = this.#timer as unknown as { unref?: () => void }\n if (typeof timer.unref === 'function') timer.unref() // don't keep the process alive\n }\n\n stop(): void {\n if (this.#timer) { clearInterval(this.#timer); this.#timer = undefined }\n }\n}\n"],"mappings":";AAqBO,IAAM,eAAN,MAAmB;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT,aAAgC;AAAA,EAChC,qBAAoC;AAAA,EACpC;AAAA,EAEA,YAAY,MAQT;AACD,SAAK,gBAAgB,KAAK;AAC1B,SAAK,SAAS,KAAK;AACnB,SAAK,YAAY,KAAK;AACtB,SAAK,aAAa,KAAK,aAAa,KAAK;AACzC,SAAK,WAAW,KAAK;AACrB,SAAK,OAAO,KAAK,QAAQ,MAAM,KAAK,IAAI;AACxC,SAAK,QAAQ,KAAK,SAAS,MAAM;AAAA,IAAC;AAAA,EACpC;AAAA;AAAA,EAGA,MAAM,OAAsB;AAC1B,UAAM,KAAK,cAAc,eAAe,KAAK,QAAQ;AAAA,MACnD,UAAU,KAAK;AAAA,MACf,WAAW,KAAK;AAAA,MAChB,UAAU,KAAK,KAAK;AAAA,MACpB,mBAAmB,KAAK;AAAA,IAC1B,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,UAAM,QAAQ,MAAM,KAAK,cAAc,UAAU,KAAK,MAAM;AAC5D,QAAI,MAAM,eAAe,KAAK,YAAY;AACxC,WAAK,aAAa,MAAM;AACxB,WAAK,MAAM,EAAE,sBAAsB,MAAM,sBAAsB,YAAY,MAAM,WAAW,CAAC;AAAA,IAC/F;AACA,QAAI,MAAM,eAAe,cAAc,KAAK,uBAAuB,MAAM,sBAAsB;AAC7F,YAAM,KAAK,SAAS;AACpB,WAAK,qBAAqB,MAAM;AAChC,YAAM,KAAK,KAAK;AAAA,IAClB;AACA,QAAI,MAAM,eAAe,UAAU;AACjC,WAAK,qBAAqB;AAAA,IAC5B;AAAA,EACF;AAAA,EAEA,MAAM,YAA0B;AAC9B,QAAI,KAAK,OAAQ;AACjB,SAAK,SAAS,YAAY,MAAM;AAAE,WAAK,KAAK,KAAK;AAAG,WAAK,KAAK,MAAM;AAAA,IAAE,GAAG,UAAU;AACnF,UAAM,QAAQ,KAAK;AACnB,QAAI,OAAO,MAAM,UAAU,WAAY,OAAM,MAAM;AAAA,EACrD;AAAA,EAEA,OAAa;AACX,QAAI,KAAK,QAAQ;AAAE,oBAAc,KAAK,MAAM;AAAG,WAAK,SAAS;AAAA,IAAU;AAAA,EACzE;AACF;","names":[]}
|
package/dist/forget/index.js
CHANGED
package/dist/forget/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/with-audit/forget/active.ts"],"sourcesContent":["/**\n * `withForget` — the active factory for GDPR right-to-erasure via\n * per-record CEK crypto-shred. Mirrors the canonical\n * `strategy.ts` (type + `NO_FORGET` sentinel) / `active.ts` (factory) split\n * used by the other services.\n *\n * @module\n */\nimport type { SubjectDeclaration, ForgetStrategy } from './strategy.js'\n\n/**\n * Declare GDPR crypto-shred for one or more collections.\n *\n * Each declared collection is forced to `perRecordKeys: true` (a shred can\n * only guarantee erasure of a record whose body is keyed off a per-record\n * CEK). On write, Noydb extracts `record[subjectField]` and maintains an\n * encrypted `_subject_index` mapping `subject → [{collection, id}]`, so\n * `vault.forget(subjectId)` can find every record for a subject and rewrite\n * each to a tombstone (body + history permanently undecryptable) while the\n * collection DEK and every other record stay intact.\n *\n * @example\n * ```ts\n * createNoydb({\n * secret, user,\n * forgetStrategy: withForget({ subjects: { invoices: 'buyerId' } }),\n * })\n * const result = await vault.forget('buyer-123')\n * // → { subject, recordsShredded, historyVersionsShredded, collections, … }\n * ```\n */\nexport function withForget(opts: SubjectDeclaration): ForgetStrategy {\n return { subjects: { ...opts.subjects }, ...(opts.scopedPurge !== undefined ? { scopedPurge: opts.scopedPurge } : {}) }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/with-audit/forget/active.ts"],"sourcesContent":["/**\n * `withForget` — the active factory for GDPR right-to-erasure via\n * per-record CEK crypto-shred. Mirrors the canonical\n * `strategy.ts` (type + `NO_FORGET` sentinel) / `active.ts` (factory) split\n * used by the other services.\n *\n * @module\n */\nimport type { SubjectDeclaration, ForgetStrategy } from './strategy.js'\n\n/**\n * Declare GDPR crypto-shred for one or more collections.\n *\n * Each declared collection is forced to `perRecordKeys: true` (a shred can\n * only guarantee erasure of a record whose body is keyed off a per-record\n * CEK). On write, Noydb extracts `record[subjectField]` and maintains an\n * encrypted `_subject_index` mapping `subject → [{collection, id}]`, so\n * `vault.forget(subjectId)` can find every record for a subject and rewrite\n * each to a tombstone (body + history permanently undecryptable) while the\n * collection DEK and every other record stay intact.\n *\n * @example\n * ```ts\n * createNoydb({\n * secret, user,\n * forgetStrategy: withForget({ subjects: { invoices: 'buyerId' } }),\n * })\n * const result = await vault.forget('buyer-123')\n * // → { subject, recordsShredded, historyVersionsShredded, collections, … }\n * ```\n */\nexport function withForget(opts: SubjectDeclaration): ForgetStrategy {\n return { subjects: { ...opts.subjects }, ...(opts.scopedPurge !== undefined ? { scopedPurge: opts.scopedPurge } : {}) }\n}\n"],"mappings":";;;;;;;;;;;;;;;AA+BO,SAAS,WAAW,MAA0C;AACnE,SAAO,EAAE,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,GAAI,KAAK,gBAAgB,SAAY,EAAE,aAAa,KAAK,YAAY,IAAI,CAAC,EAAG;AACxH;","names":[]}
|
package/dist/guards/index.js
CHANGED
package/dist/history/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/with-commit/history/active.ts"],"sourcesContent":["/**\n * Active history strategy — `withHistory()` returns the real\n * implementation that wires per-record snapshots, the hash-chained\n * audit ledger, JSON-patch deltas, and time-machine reads into the\n * core write/read paths.\n *\n * Consumers opt in by:\n *\n * ```ts\n * import { createNoydb } from '@noy-db/hub'\n * import { withHistory } from '@noy-db/hub/history'\n *\n * const db = await createNoydb({\n * store: ...,\n * user: ...,\n * historyStrategy: withHistory(),\n * })\n * ```\n *\n * The factory is a thin wrapper that delegates to the existing\n * `history.ts`, `diff.ts`, `ledger/store.ts`, `ledger/patch.ts`, and\n * `time-machine.ts` modules. Splitting the import chain through this\n * file is what lets `tsup` tree-shake the heavy modules out of the\n * default `@noy-db/hub` bundle when no `withHistory()` import is\n * present in the consumer.\n *\n * @public\n */\n\nimport type { HistoryStrategy, BuildLedgerOptions } from './strategy.js'\nimport {\n saveHistory,\n historyIdentity,\n getHistory,\n getVersionEnvelope,\n pruneHistory,\n clearHistory,\n tombstoneHistory,\n rewrapHistory,\n} from './history.js'\nimport { diff as computeDiff } from './diff.js'\nimport { LedgerStore, envelopePayloadHash } from './ledger/store.js'\nimport { computePatch } from './ledger/patch.js'\nimport { VaultInstant } from './time-machine.js'\n\n/**\n * Build the active history strategy. Today the factory takes no\n * options; per-collection retention tuning still flows through\n * `HistoryConfig` on `Vault.collection()` / `vault.openVault()`.\n *\n * Future option slots (kept off the LTS surface for now):\n * - global maxVersions cap\n * - global beforeDate prune cadence\n * - ledger encryption toggle (today inferred from vault.encrypted)\n */\nexport function withHistory(): HistoryStrategy {\n return {\n saveHistory,\n historyIdentity,\n getHistoryEntries: getHistory,\n getVersionEnvelope,\n pruneHistory,\n clearHistory,\n tombstoneHistory,\n rewrapHistory,\n envelopePayloadHash,\n computePatch,\n diff: computeDiff,\n buildLedger(opts: BuildLedgerOptions): LedgerStore {\n return new LedgerStore({\n adapter: opts.adapter,\n vault: opts.vault,\n encrypted: opts.encrypted,\n getDEK: opts.getDEK,\n actor: opts.actor,\n })\n },\n buildVaultInstant(engine, timestamp): VaultInstant {\n return new VaultInstant(engine, timestamp)\n },\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/with-commit/history/active.ts"],"sourcesContent":["/**\n * Active history strategy — `withHistory()` returns the real\n * implementation that wires per-record snapshots, the hash-chained\n * audit ledger, JSON-patch deltas, and time-machine reads into the\n * core write/read paths.\n *\n * Consumers opt in by:\n *\n * ```ts\n * import { createNoydb } from '@noy-db/hub'\n * import { withHistory } from '@noy-db/hub/history'\n *\n * const db = await createNoydb({\n * store: ...,\n * user: ...,\n * historyStrategy: withHistory(),\n * })\n * ```\n *\n * The factory is a thin wrapper that delegates to the existing\n * `history.ts`, `diff.ts`, `ledger/store.ts`, `ledger/patch.ts`, and\n * `time-machine.ts` modules. Splitting the import chain through this\n * file is what lets `tsup` tree-shake the heavy modules out of the\n * default `@noy-db/hub` bundle when no `withHistory()` import is\n * present in the consumer.\n *\n * @public\n */\n\nimport type { HistoryStrategy, BuildLedgerOptions } from './strategy.js'\nimport {\n saveHistory,\n historyIdentity,\n getHistory,\n getVersionEnvelope,\n pruneHistory,\n clearHistory,\n tombstoneHistory,\n rewrapHistory,\n} from './history.js'\nimport { diff as computeDiff } from './diff.js'\nimport { LedgerStore, envelopePayloadHash } from './ledger/store.js'\nimport { computePatch } from './ledger/patch.js'\nimport { VaultInstant } from './time-machine.js'\n\n/**\n * Build the active history strategy. Today the factory takes no\n * options; per-collection retention tuning still flows through\n * `HistoryConfig` on `Vault.collection()` / `vault.openVault()`.\n *\n * Future option slots (kept off the LTS surface for now):\n * - global maxVersions cap\n * - global beforeDate prune cadence\n * - ledger encryption toggle (today inferred from vault.encrypted)\n */\nexport function withHistory(): HistoryStrategy {\n return {\n saveHistory,\n historyIdentity,\n getHistoryEntries: getHistory,\n getVersionEnvelope,\n pruneHistory,\n clearHistory,\n tombstoneHistory,\n rewrapHistory,\n envelopePayloadHash,\n computePatch,\n diff: computeDiff,\n buildLedger(opts: BuildLedgerOptions): LedgerStore {\n return new LedgerStore({\n adapter: opts.adapter,\n vault: opts.vault,\n encrypted: opts.encrypted,\n getDEK: opts.getDEK,\n actor: opts.actor,\n })\n },\n buildVaultInstant(engine, timestamp): VaultInstant {\n return new VaultInstant(engine, timestamp)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDO,SAAS,cAA+B;AAC7C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY,MAAuC;AACjD,aAAO,IAAI,YAAY;AAAA,QACrB,SAAS,KAAK;AAAA,QACd,OAAO,KAAK;AAAA,QACZ,WAAW,KAAK;AAAA,QAChB,QAAQ,KAAK;AAAA,QACb,OAAO,KAAK;AAAA,MACd,CAAC;AAAA,IACH;AAAA,IACA,kBAAkB,QAAQ,WAAyB;AACjD,aAAO,IAAI,aAAa,QAAQ,SAAS;AAAA,IAC3C;AAAA,EACF;AACF;","names":[]}
|
package/dist/i18n/index.js
CHANGED
package/dist/i18n/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/via/i18n/densify.ts","../../src/via/i18n/active.ts"],"sourcesContent":["/**\n * `densifyOnWrite` — eager-fill empty i18n slots from each field's\n * substitute chain at write time, recording provenance in the internal\n * `_i18nFilled` marker (decision A: dense map + prior-read).\n *\n * Pure functions over plain records; all DB wiring lives in collection.ts.\n * Only single-leaf i18n fields are supported (array-wildcard paths are\n * skipped, mirroring auto-translate).\n */\nimport { getAtPath, resolveI18nText } from './core.js'\nimport type { I18nTextDescriptor } from './core.js'\n\nconst MARKER = '_i18nFilled'\n\ntype Leaf = Record<string, string>\n\n/** Locales recorded as densify-filled on the prior record for `field`. */\nfunction priorFilled(prior: Record<string, unknown> | undefined, field: string): Set<string> {\n if (!prior) return new Set()\n const marker = prior[MARKER] as Record<string, string[]> | undefined\n return new Set(marker?.[field] ?? [])\n}\n\n/** The single leaf map for `field`, or undefined for absent / array-wildcard / non-object. */\nfunction singleLeaf(record: Record<string, unknown>, field: string): Leaf | undefined {\n const leaves = getAtPath(record, field)\n if (leaves.length !== 1) return undefined\n const leaf = leaves[0]\n if (!leaf || typeof leaf !== 'object' || Array.isArray(leaf)) return undefined\n return leaf as Leaf\n}\n\n/**\n * Per field, the locales that are UNCHANGED round-tripped densify fills — i.e.\n * marked filled on the prior record AND identical in the incoming record.\n * These are exempt from write-time script enforcement (they are derived copies,\n * not authored text). Slots the user changed are NOT exempt (validated as authored).\n */\nexport function computeExemptFills(\n prior: Record<string, unknown> | undefined,\n incoming: Record<string, unknown>,\n fields: Record<string, I18nTextDescriptor>,\n): Map<string, Set<string>> {\n const out = new Map<string, Set<string>>()\n if (!prior) return out\n for (const field of Object.keys(fields)) {\n const filled = priorFilled(prior, field)\n if (filled.size === 0) continue\n const priorLeaf = singleLeaf(prior, field)\n const incLeaf = singleLeaf(incoming, field)\n if (!priorLeaf || !incLeaf) continue\n const exempt = new Set<string>()\n for (const loc of filled) {\n if (incLeaf[loc] !== undefined && incLeaf[loc] !== '' && incLeaf[loc] === priorLeaf[loc]) {\n exempt.add(loc)\n }\n }\n if (exempt.size > 0) out.set(field, exempt)\n }\n return out\n}\n\n/**\n * Mutate `record` in place: fill empty declared-language slots for each\n * densify-enabled field from the field's substitute chain, recompute unchanged\n * prior fills, clear marks for slots that became authored, and write the\n * resulting `_i18nFilled` marker (removed when empty).\n *\n * `prior` is read-only — it is never mutated.\n *\n * Provenance uses value-equality (decision A): a slot counts as an unchanged\n * fill when it was prior-marked AND its value still equals the prior value. A\n * consequence is that re-authoring a value byte-identical to the existing fill\n * keeps it classified as a fill (the visible value is unchanged either way; the\n * slot stays script-exempt and will auto-refresh if its source later changes).\n * This is inherent to value-equality provenance, not a bug.\n */\nexport function densify(\n record: Record<string, unknown>,\n prior: Record<string, unknown> | undefined,\n fields: Record<string, I18nTextDescriptor>,\n): void {\n let marker = record[MARKER] as Record<string, string[]> | undefined\n\n for (const [field, descriptor] of Object.entries(fields)) {\n const leaf = singleLeaf(record, field)\n if (!leaf) continue\n const { languages, substitute, smartSubstitute } = descriptor.options\n const filledSet = priorFilled(prior, field)\n const priorLeaf = singleLeaf(prior ?? {}, field)\n const isUnchangedFill = (loc: string): boolean =>\n filledSet.has(loc) && priorLeaf?.[loc] !== undefined && leaf[loc] === priorLeaf[loc]\n\n // Authored source = present, non-empty, NOT an unchanged prior fill.\n const authored: Leaf = {}\n for (const [loc, val] of Object.entries(leaf)) {\n if (typeof val === 'string' && val !== '' && !isUnchangedFill(loc)) authored[loc] = val\n }\n\n const filled: string[] = []\n for (const loc of languages) {\n if (authored[loc] !== undefined) continue // real value present → not a fill\n const sub = resolveI18nText(authored, loc, undefined, field, {\n policy: 'substitute',\n substitute: substitute ?? ['any'],\n ...(smartSubstitute ? { smartSubstitute } : {}),\n })\n if (typeof sub === 'string' && sub !== '') {\n leaf[loc] = sub\n filled.push(loc)\n } else if (isUnchangedFill(loc)) {\n delete leaf[loc] // stale fill with no source left → drop it\n }\n }\n\n if (filled.length > 0) {\n marker ??= {}\n marker[field] = filled\n } else if (marker) {\n delete marker[field]\n }\n }\n\n if (marker && Object.keys(marker).length > 0) {\n record[MARKER] = marker\n } else {\n delete record[MARKER]\n }\n}\n","/**\n * Active i18n strategy — `withI18n()` returns the real implementation\n * that wires multi-locale resolution, i18nText validation, and the\n * `DictionaryHandle` for `dictKey` fields into the core read/write\n * paths.\n *\n * Consumers opt in by:\n *\n * ```ts\n * import { createNoydb } from '@noy-db/hub'\n * import { withI18n } from '@noy-db/hub/i18n'\n *\n * const db = await createNoydb({\n * store: ...,\n * user: ...,\n * i18nStrategy: withI18n(),\n * })\n * ```\n *\n * The factory delegates to the existing `core.ts` and `dictionary.ts`\n * modules. Splitting the import chain through this file is what lets\n * tsup tree-shake the `~854 LOC` of dictionary + locale resolution\n * out of the default bundle when no `withI18n()` import is present.\n *\n * @public\n */\n\nimport type { I18nStrategy, BuildDictionaryHandleOptions } from './strategy.js'\nimport { applyI18nLocale, validateI18nTextValue } from './core.js'\nimport { enforceScript } from './script.js'\nimport { computeExemptFills, densify } from './densify.js'\nimport type { DictionaryHandle } from './dictionary.js'\nimport { withLookup } from '../lookup/active.js'\n\nexport function withI18n(): I18nStrategy {\n // #650 Task 1: the handle-construction logic moved to\n // via/lookup/active.ts (LookupHandle, renamed from\n // DictionaryHandle, new home). buildDictionaryHandle below is now a\n // thin field-name translator over buildLookupHandle — same handle.\n const lookup = withLookup()\n return {\n applyI18nLocale,\n validateI18nTextValue,\n enforceScript,\n computeExemptFills,\n densify,\n buildDictionaryHandle<Keys extends string = string>(\n opts: BuildDictionaryHandleOptions<Keys>,\n ): DictionaryHandle<Keys> {\n return lookup.buildLookupHandle<Keys>({\n adapter: opts.adapter,\n compartmentName: opts.compartmentName,\n dimensionName: opts.dictionaryName,\n keyring: opts.keyring,\n reservedEnvelopes: opts.reservedEnvelopes,\n encrypted: opts.encrypted,\n ledger: opts.ledger,\n options: opts.options,\n findAndUpdateReferences: opts.findAndUpdateReferences,\n emitter: opts.emitter,\n buildDeleteMarker: opts.buildDeleteMarker, // #647 fix wave 1\n onDirty: opts.onDirty, // #650 Task 4\n onRecordMutated: opts.onRecordMutated, // #650 Task 4\n checkReferencesOnDelete: opts.checkReferencesOnDelete, // #650 Task 5\n })\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/via/i18n/densify.ts","../../src/via/i18n/active.ts"],"sourcesContent":["/**\n * `densifyOnWrite` — eager-fill empty i18n slots from each field's\n * substitute chain at write time, recording provenance in the internal\n * `_i18nFilled` marker (decision A: dense map + prior-read).\n *\n * Pure functions over plain records; all DB wiring lives in collection.ts.\n * Only single-leaf i18n fields are supported (array-wildcard paths are\n * skipped, mirroring auto-translate).\n */\nimport { getAtPath, resolveI18nText } from './core.js'\nimport type { I18nTextDescriptor } from './core.js'\n\nconst MARKER = '_i18nFilled'\n\ntype Leaf = Record<string, string>\n\n/** Locales recorded as densify-filled on the prior record for `field`. */\nfunction priorFilled(prior: Record<string, unknown> | undefined, field: string): Set<string> {\n if (!prior) return new Set()\n const marker = prior[MARKER] as Record<string, string[]> | undefined\n return new Set(marker?.[field] ?? [])\n}\n\n/** The single leaf map for `field`, or undefined for absent / array-wildcard / non-object. */\nfunction singleLeaf(record: Record<string, unknown>, field: string): Leaf | undefined {\n const leaves = getAtPath(record, field)\n if (leaves.length !== 1) return undefined\n const leaf = leaves[0]\n if (!leaf || typeof leaf !== 'object' || Array.isArray(leaf)) return undefined\n return leaf as Leaf\n}\n\n/**\n * Per field, the locales that are UNCHANGED round-tripped densify fills — i.e.\n * marked filled on the prior record AND identical in the incoming record.\n * These are exempt from write-time script enforcement (they are derived copies,\n * not authored text). Slots the user changed are NOT exempt (validated as authored).\n */\nexport function computeExemptFills(\n prior: Record<string, unknown> | undefined,\n incoming: Record<string, unknown>,\n fields: Record<string, I18nTextDescriptor>,\n): Map<string, Set<string>> {\n const out = new Map<string, Set<string>>()\n if (!prior) return out\n for (const field of Object.keys(fields)) {\n const filled = priorFilled(prior, field)\n if (filled.size === 0) continue\n const priorLeaf = singleLeaf(prior, field)\n const incLeaf = singleLeaf(incoming, field)\n if (!priorLeaf || !incLeaf) continue\n const exempt = new Set<string>()\n for (const loc of filled) {\n if (incLeaf[loc] !== undefined && incLeaf[loc] !== '' && incLeaf[loc] === priorLeaf[loc]) {\n exempt.add(loc)\n }\n }\n if (exempt.size > 0) out.set(field, exempt)\n }\n return out\n}\n\n/**\n * Mutate `record` in place: fill empty declared-language slots for each\n * densify-enabled field from the field's substitute chain, recompute unchanged\n * prior fills, clear marks for slots that became authored, and write the\n * resulting `_i18nFilled` marker (removed when empty).\n *\n * `prior` is read-only — it is never mutated.\n *\n * Provenance uses value-equality (decision A): a slot counts as an unchanged\n * fill when it was prior-marked AND its value still equals the prior value. A\n * consequence is that re-authoring a value byte-identical to the existing fill\n * keeps it classified as a fill (the visible value is unchanged either way; the\n * slot stays script-exempt and will auto-refresh if its source later changes).\n * This is inherent to value-equality provenance, not a bug.\n */\nexport function densify(\n record: Record<string, unknown>,\n prior: Record<string, unknown> | undefined,\n fields: Record<string, I18nTextDescriptor>,\n): void {\n let marker = record[MARKER] as Record<string, string[]> | undefined\n\n for (const [field, descriptor] of Object.entries(fields)) {\n const leaf = singleLeaf(record, field)\n if (!leaf) continue\n const { languages, substitute, smartSubstitute } = descriptor.options\n const filledSet = priorFilled(prior, field)\n const priorLeaf = singleLeaf(prior ?? {}, field)\n const isUnchangedFill = (loc: string): boolean =>\n filledSet.has(loc) && priorLeaf?.[loc] !== undefined && leaf[loc] === priorLeaf[loc]\n\n // Authored source = present, non-empty, NOT an unchanged prior fill.\n const authored: Leaf = {}\n for (const [loc, val] of Object.entries(leaf)) {\n if (typeof val === 'string' && val !== '' && !isUnchangedFill(loc)) authored[loc] = val\n }\n\n const filled: string[] = []\n for (const loc of languages) {\n if (authored[loc] !== undefined) continue // real value present → not a fill\n const sub = resolveI18nText(authored, loc, undefined, field, {\n policy: 'substitute',\n substitute: substitute ?? ['any'],\n ...(smartSubstitute ? { smartSubstitute } : {}),\n })\n if (typeof sub === 'string' && sub !== '') {\n leaf[loc] = sub\n filled.push(loc)\n } else if (isUnchangedFill(loc)) {\n delete leaf[loc] // stale fill with no source left → drop it\n }\n }\n\n if (filled.length > 0) {\n marker ??= {}\n marker[field] = filled\n } else if (marker) {\n delete marker[field]\n }\n }\n\n if (marker && Object.keys(marker).length > 0) {\n record[MARKER] = marker\n } else {\n delete record[MARKER]\n }\n}\n","/**\n * Active i18n strategy — `withI18n()` returns the real implementation\n * that wires multi-locale resolution, i18nText validation, and the\n * `DictionaryHandle` for `dictKey` fields into the core read/write\n * paths.\n *\n * Consumers opt in by:\n *\n * ```ts\n * import { createNoydb } from '@noy-db/hub'\n * import { withI18n } from '@noy-db/hub/i18n'\n *\n * const db = await createNoydb({\n * store: ...,\n * user: ...,\n * i18nStrategy: withI18n(),\n * })\n * ```\n *\n * The factory delegates to the existing `core.ts` and `dictionary.ts`\n * modules. Splitting the import chain through this file is what lets\n * tsup tree-shake the `~854 LOC` of dictionary + locale resolution\n * out of the default bundle when no `withI18n()` import is present.\n *\n * @public\n */\n\nimport type { I18nStrategy, BuildDictionaryHandleOptions } from './strategy.js'\nimport { applyI18nLocale, validateI18nTextValue } from './core.js'\nimport { enforceScript } from './script.js'\nimport { computeExemptFills, densify } from './densify.js'\nimport type { DictionaryHandle } from './dictionary.js'\nimport { withLookup } from '../lookup/active.js'\n\nexport function withI18n(): I18nStrategy {\n // #650 Task 1: the handle-construction logic moved to\n // via/lookup/active.ts (LookupHandle, renamed from\n // DictionaryHandle, new home). buildDictionaryHandle below is now a\n // thin field-name translator over buildLookupHandle — same handle.\n const lookup = withLookup()\n return {\n applyI18nLocale,\n validateI18nTextValue,\n enforceScript,\n computeExemptFills,\n densify,\n buildDictionaryHandle<Keys extends string = string>(\n opts: BuildDictionaryHandleOptions<Keys>,\n ): DictionaryHandle<Keys> {\n return lookup.buildLookupHandle<Keys>({\n adapter: opts.adapter,\n compartmentName: opts.compartmentName,\n dimensionName: opts.dictionaryName,\n keyring: opts.keyring,\n reservedEnvelopes: opts.reservedEnvelopes,\n encrypted: opts.encrypted,\n ledger: opts.ledger,\n options: opts.options,\n findAndUpdateReferences: opts.findAndUpdateReferences,\n emitter: opts.emitter,\n buildDeleteMarker: opts.buildDeleteMarker, // #647 fix wave 1\n onDirty: opts.onDirty, // #650 Task 4\n onRecordMutated: opts.onRecordMutated, // #650 Task 4\n checkReferencesOnDelete: opts.checkReferencesOnDelete, // #650 Task 5\n })\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAYA,IAAM,SAAS;AAKf,SAAS,YAAY,OAA4C,OAA4B;AAC3F,MAAI,CAAC,MAAO,QAAO,oBAAI,IAAI;AAC3B,QAAM,SAAS,MAAM,MAAM;AAC3B,SAAO,IAAI,IAAI,SAAS,KAAK,KAAK,CAAC,CAAC;AACtC;AAGA,SAAS,WAAW,QAAiC,OAAiC;AACpF,QAAM,SAAS,UAAU,QAAQ,KAAK;AACtC,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,OAAO,OAAO,CAAC;AACrB,MAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,EAAG,QAAO;AACrE,SAAO;AACT;AAQO,SAAS,mBACd,OACA,UACA,QAC0B;AAC1B,QAAM,MAAM,oBAAI,IAAyB;AACzC,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,SAAS,OAAO,KAAK,MAAM,GAAG;AACvC,UAAM,SAAS,YAAY,OAAO,KAAK;AACvC,QAAI,OAAO,SAAS,EAAG;AACvB,UAAM,YAAY,WAAW,OAAO,KAAK;AACzC,UAAM,UAAU,WAAW,UAAU,KAAK;AAC1C,QAAI,CAAC,aAAa,CAAC,QAAS;AAC5B,UAAM,SAAS,oBAAI,IAAY;AAC/B,eAAW,OAAO,QAAQ;AACxB,UAAI,QAAQ,GAAG,MAAM,UAAa,QAAQ,GAAG,MAAM,MAAM,QAAQ,GAAG,MAAM,UAAU,GAAG,GAAG;AACxF,eAAO,IAAI,GAAG;AAAA,MAChB;AAAA,IACF;AACA,QAAI,OAAO,OAAO,EAAG,KAAI,IAAI,OAAO,MAAM;AAAA,EAC5C;AACA,SAAO;AACT;AAiBO,SAAS,QACd,QACA,OACA,QACM;AACN,MAAI,SAAS,OAAO,MAAM;AAE1B,aAAW,CAAC,OAAO,UAAU,KAAK,OAAO,QAAQ,MAAM,GAAG;AACxD,UAAM,OAAO,WAAW,QAAQ,KAAK;AACrC,QAAI,CAAC,KAAM;AACX,UAAM,EAAE,WAAW,YAAY,gBAAgB,IAAI,WAAW;AAC9D,UAAM,YAAY,YAAY,OAAO,KAAK;AAC1C,UAAM,YAAY,WAAW,SAAS,CAAC,GAAG,KAAK;AAC/C,UAAM,kBAAkB,CAAC,QACvB,UAAU,IAAI,GAAG,KAAK,YAAY,GAAG,MAAM,UAAa,KAAK,GAAG,MAAM,UAAU,GAAG;AAGrF,UAAM,WAAiB,CAAC;AACxB,eAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC7C,UAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,CAAC,gBAAgB,GAAG,EAAG,UAAS,GAAG,IAAI;AAAA,IACtF;AAEA,UAAM,SAAmB,CAAC;AAC1B,eAAW,OAAO,WAAW;AAC3B,UAAI,SAAS,GAAG,MAAM,OAAW;AACjC,YAAM,MAAM,gBAAgB,UAAU,KAAK,QAAW,OAAO;AAAA,QAC3D,QAAQ;AAAA,QACR,YAAY,cAAc,CAAC,KAAK;AAAA,QAChC,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;AAAA,MAC/C,CAAC;AACD,UAAI,OAAO,QAAQ,YAAY,QAAQ,IAAI;AACzC,aAAK,GAAG,IAAI;AACZ,eAAO,KAAK,GAAG;AAAA,MACjB,WAAW,gBAAgB,GAAG,GAAG;AAC/B,eAAO,KAAK,GAAG;AAAA,MACjB;AAAA,IACF;AAEA,QAAI,OAAO,SAAS,GAAG;AACrB,iBAAW,CAAC;AACZ,aAAO,KAAK,IAAI;AAAA,IAClB,WAAW,QAAQ;AACjB,aAAO,OAAO,KAAK;AAAA,IACrB;AAAA,EACF;AAEA,MAAI,UAAU,OAAO,KAAK,MAAM,EAAE,SAAS,GAAG;AAC5C,WAAO,MAAM,IAAI;AAAA,EACnB,OAAO;AACL,WAAO,OAAO,MAAM;AAAA,EACtB;AACF;;;AC9FO,SAAS,WAAyB;AAKvC,QAAM,SAAS,WAAW;AAC1B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,sBACE,MACwB;AACxB,aAAO,OAAO,kBAAwB;AAAA,QACpC,SAAS,KAAK;AAAA,QACd,iBAAiB,KAAK;AAAA,QACtB,eAAe,KAAK;AAAA,QACpB,SAAS,KAAK;AAAA,QACd,mBAAmB,KAAK;AAAA,QACxB,WAAW,KAAK;AAAA,QAChB,QAAQ,KAAK;AAAA,QACb,SAAS,KAAK;AAAA,QACd,yBAAyB,KAAK;AAAA,QAC9B,SAAS,KAAK;AAAA,QACd,mBAAmB,KAAK;AAAA;AAAA,QACxB,SAAS,KAAK;AAAA;AAAA,QACd,iBAAiB,KAAK;AAAA;AAAA,QACtB,yBAAyB,KAAK;AAAA;AAAA,MAChC,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
|