• DcRouter v32.1.2
    Release / build-and-release (push) Successful in 25m28s
    Stable

    jkunz released this 2026-09-19 08:05:04 +00:00 | 70 commits to main since this release

    2026-09-19 - 32.1.2

    Fixes

    • keep the engineering notes (hints.md) out of the published image (docker): .dockerignore now excludes it, the way it already excludes readme.plan.md and the test tree.

    • retire the last three raw-driver sites in ts/ (db, email, monitoring): @lossless.org/client moves to 1.1.0 exactly, whose document model adds the four capabilities these sites were waiting for. No MongoDB driver handle remains in ts/; ts_migrations/ is the only place in the repository that holds one.

      • OpsConfigEventDoc.upsertOpenEvent() uses atomicUpdate(..., { upsert: true }) anchored on the sparse unique openDedupeKey, seeding the @unI() identity through $setOnInsert. The client refuses an identity seed without upsert: true, refuses $set on an identity at all, and refuses a seed that contradicts an identity anchor in the filter, so deduplication keeps exactly the guarantee the raw upsert had: one open occurrence per key, its identity fixed by the insert that won.
      • EmailTrafficBucketDoc.persistAbsolute() uses atomicUpsertMany() — one unordered batch, bounded by the client at 1..1000 operations and 16 MiB, which a 500-bucket flush always fits. Each operation is anchored on the declared unique bucketStart, uses $max for the monotonic counters so a replayed absolute snapshot can never lower one, and seeds the identity through $setOnInsert.
      • SmartMtaStorageDoc declares identityAsDocumentId: 'id', so the content address (sha256(normalizedKey)) that has always been this collection's _id is now the declared identity as well, and SmartMtaStorageManager addresses every document through it instead of a raw collection handle. This declaration is irreversible: reads are answered through _id from here on, so a row whose _id is not its identity would stop resolving. It is safe because dcrouter has only ever written _id === id === sha256(normalizedKey) here. The model declares no unique index on id — the client refuses one for a field that owns the document _id, since the primary key is the only uniqueness authority such a field may have — and every upsert must pin the identity by equality, which the client enforces rather than letting a generated _id contradict the mapping. SmartMtaStorageManager no longer takes a database: the document model owns its binding.
      • One persisted-byte difference, in SmartMtaStorageDoc only: documents written from here on also carry the ORM's own _createdAt/_updatedAt bookkeeping, which the raw writes did not produce. Every application member (_id, id, key, value, createdAt, updatedAt), the string _id shape, _id === id and the index set (_id_ alone) are unchanged, and no dcrouter code reads the two ORM members. They cannot be avoided through any client API — atomicUpdate writes _updatedAt unconditionally — so existing rows simply lack them until their next write. OpsConfigEventDoc and EmailTrafficBucketDoc move no byte at all: member sets, values, ObjectId primary ids and index names, keys and options are identical, captured from main and compared through inspectStoredDocuments()/inspectNamespace().
      • New specs: a concurrent open event coalescing onto one seeded identity and the three identity-seeding refusals (test.ops-config-events.node.ts), a 500-bucket flush of mixed inserts and regressed updates keeping $max monotonic (test.monitoring-events.node.ts), content-addressed lookup, overwrite and delete by identity plus the unpinned-upsert refusal (test.smartmta-storage-manager.node.ts), and the stored envelope of the content-addressed model (test.nosqldb-persisted-bytes.node.ts). The remaining raw index and document reads in those three spec files move to inspectNamespace()/inspectStoredDocuments().
    • route Web Push persistence through the document client (webpush, db) No persisted byte moves and no migration step: the stored member set, the ORM-assigned ObjectId primary id, _createdAt/_updatedAt and every index name, key and option are unchanged, proven by test/test.webpush-persisted-bytes.node.ts, which compares the stored BSON against the member sets captured from the pre-conversion implementation and the live index topology against ts/webpush/webpush-indexes.ts.

      • WebPushBindingDoc, WebPushAdmissionDoc and WebPushSpoolDoc drop their getNativeCollection() accessors and declare their full index topology with @namedIndex under the exact names the released web-push-provider-schema migration created. Binding a model re-creates any declared index the namespace is missing (a foreground index build on a live collection) and verifies the rest; the migration keeps owning index creation for existing databases, and the declarations exist so the topology is checked, not so application code defines it.
      • The three models declare no @unI() identity: the ORM hard-codes an identity field's unique index name to <field>_1, which would add a second unique index over the same key as web_push_binding_id_unique / web_push_spool_id_unique. atomicDelete is anchored by the declared unique indexes instead, which the ORM accepts as globally constraining.
      • All ~80 native operations in ts/webpush/classes.webpush-manager.ts move to getInstance/exists/getCursor/getCount/atomicUpdate/atomicUpdateMany/atomicFindOneAndUpdate/atomicDelete/atomicDeleteMany, keeping every compare-and-set fence, lease token, controller generation and bounded batch. _updatedAt is no longer hand-written by 15 call sites; the ORM owns it.
      • The delivery claim's $or now carries one branch per claimable state instead of state: { $in: [...] }. The ORM requires an equality anchor in every branch of a write filter; the branches match exactly the same documents and still ride web_push_spool_delivery_scan.
      • Startup asserts the index topology through the client's collection-topology inspection and requires an exact match: a renamed or altered index fails closed as before, and an index nobody declared now fails closed too, so a hand-added index on web_push_binding, web_push_admission or web_push_spool stops dcrouter from starting. Remedy: drop the extra index (db.<collection>.dropIndex('<name>')) or add it to the migration and the model declaration. A declared index that is missing is re-created on bind, not refused.
      • Fixes a defect introduced with 32.1.1: the binding, spool and admission insert races tested the MongoDB driver's code === 11000, but the client reports a violated unique index as SmartdataPersistenceError('unique_conflict'), so those races rethrew instead of recovering.
    • route production document access through the client's public API instead of the raw MongoDB driver (db, dns, security, monitoring)

      • SmartdataDb.mongoDb / SmartdataCollection.mongoDbCollection are no longer used for ordinary reads and writes in ts/. CachedEmail, GatewayClientDoc, OpsConfigEventDoc, EmailTrafficBucketDoc, IpIntelligenceDoc and DnsManager.migrateToDcrouter now use getCursor, getPagedInstances, getCount, atomicUpdate, atomicUpdateMany, atomicDelete and atomicDeleteMany. Every fence is preserved: the gateway-client compare-and-set still matches exactly one revision, plural deletes anchor on the scanned identities and re-evaluate their range guard server-side, and the DNS provider-link removal stays inside the migration transaction.
      • IpIntelligenceDoc.findOldestIds()/deleteByDocumentIds() become findOldestIpAddresses()/deleteByIpAddresses(): retention now addresses records by their immutable ipAddress identity instead of opaque _id values. SecurityPolicyManager retention is unchanged in behaviour, batch sizes and ordering.
      • Three sites kept a raw handle with a WHY comment naming the @lossless.org/client capability they were waiting for: EmailTrafficBucketDoc.persistAbsolute() (needs $max/$min, an identity-seeding $setOnInsert and a plural upsert), OpsConfigEventDoc.upsertOpenEvent() (needs the identity-seeding $setOnInsert) and SmartMtaStorageManager (needs a caller-supplied deterministic _id). @lossless.org/client 1.1.0 shipped all four capabilities and the first entry above retires those handles in this same release. The Web Push persistence conversion is released alongside it: no client capability is missing there, but the three models declare no identity or index topology, so converting them changes save() behaviour for live collections.
      • New specs: bucket retention ordering and batching (test.monitoring-events.node.ts), IP intelligence oldest-first scanning and identity-anchored deletion (test.security-policy-manager.node.ts), and the ops-config event paging/prune bounds now run against a real database instead of a faked native collection (test.ops-config-events.node.ts).
    Downloads