@serve.zone/dcrouter 32.1.1 → 32.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/deno.json +1 -1
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.js +2 -2
  4. package/dist_ts/config/classes.gateway-client-manager.js +1 -1
  5. package/dist_ts/db/documents/classes.cached.email.d.ts +0 -1
  6. package/dist_ts/db/documents/classes.cached.email.js +45 -48
  7. package/dist_ts/db/documents/classes.email-traffic-bucket.doc.d.ts +0 -1
  8. package/dist_ts/db/documents/classes.email-traffic-bucket.doc.js +61 -50
  9. package/dist_ts/db/documents/classes.gateway-client.doc.d.ts +9 -2
  10. package/dist_ts/db/documents/classes.gateway-client.doc.js +9 -11
  11. package/dist_ts/db/documents/classes.ip-intelligence.doc.d.ts +4 -3
  12. package/dist_ts/db/documents/classes.ip-intelligence.doc.js +27 -31
  13. package/dist_ts/db/documents/classes.ops-config-event.doc.d.ts +0 -2
  14. package/dist_ts/db/documents/classes.ops-config-event.doc.js +70 -68
  15. package/dist_ts/db/documents/classes.smartmta-storage.doc.d.ts +14 -0
  16. package/dist_ts/db/documents/classes.smartmta-storage.doc.js +17 -3
  17. package/dist_ts/db/documents/classes.webpush-admission.doc.d.ts +0 -1
  18. package/dist_ts/db/documents/classes.webpush-admission.doc.js +14 -5
  19. package/dist_ts/db/documents/classes.webpush-binding.doc.d.ts +1 -1
  20. package/dist_ts/db/documents/classes.webpush-binding.doc.js +46 -8
  21. package/dist_ts/db/documents/classes.webpush-spool.doc.d.ts +0 -1
  22. package/dist_ts/db/documents/classes.webpush-spool.doc.js +34 -6
  23. package/dist_ts/dns/manager.dns.js +7 -3
  24. package/dist_ts/email/classes.smartmta-storage-manager.d.ts +15 -3
  25. package/dist_ts/email/classes.smartmta-storage-manager.js +62 -36
  26. package/dist_ts/security/classes.security-policy-manager.js +11 -9
  27. package/dist_ts/webpush/classes.webpush-manager.d.ts +5 -1
  28. package/dist_ts/webpush/classes.webpush-manager.js +162 -232
  29. package/dist_ts_web/00_commitinfo_data.js +1 -1
  30. package/package.json +3 -3
  31. package/readme.md +1 -0
  32. package/ts/00_commitinfo_data.ts +1 -1
  33. package/ts/classes.dcrouter.ts +1 -1
  34. package/ts/config/classes.gateway-client-manager.ts +4 -2
  35. package/ts/db/documents/classes.cached.email.ts +51 -66
  36. package/ts/db/documents/classes.email-traffic-bucket.doc.ts +65 -59
  37. package/ts/db/documents/classes.gateway-client.doc.ts +38 -18
  38. package/ts/db/documents/classes.ip-intelligence.doc.ts +37 -40
  39. package/ts/db/documents/classes.ops-config-event.doc.ts +80 -77
  40. package/ts/db/documents/classes.smartmta-storage.doc.ts +16 -1
  41. package/ts/db/documents/classes.webpush-admission.doc.ts +14 -5
  42. package/ts/db/documents/classes.webpush-binding.doc.ts +54 -8
  43. package/ts/db/documents/classes.webpush-spool.doc.ts +40 -6
  44. package/ts/dns/manager.dns.ts +6 -2
  45. package/ts/email/classes.smartmta-storage-manager.ts +66 -50
  46. package/ts/security/classes.security-policy-manager.ts +10 -8
  47. package/ts/webpush/classes.webpush-manager.ts +175 -280
  48. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -23,11 +23,6 @@ import {
23
23
  type IWebPushTransport,
24
24
  } from './classes.webpush-transport.js';
25
25
  import { WebPushEndpointPolicyError } from './helpers.webpush-endpoint.js';
26
- import {
27
- getWebPushIndexSpecifications,
28
- type IWebPushIndexSpecification,
29
- type TWebPushIndexCollection,
30
- } from './webpush-indexes.js';
31
26
 
32
27
  type IWebPushBinding = plugins.servezoneInterfaces.data.IWebPushBinding;
33
28
  type IWebPushCredentialOneTimeSecret =
@@ -148,6 +143,8 @@ const NONTERMINAL_STATES: TWebPushDeliveryState[] = [
148
143
  'sending',
149
144
  'deferred',
150
145
  ];
146
+ /** Non-leased states a delivery worker may claim. `sending` is claimed only on lease expiry. */
147
+ const CLAIMABLE_IDLE_STATES = ['accepted', 'queued', 'deferred'] as const;
151
148
 
152
149
  function requireNonEmptyString(
153
150
  valueArg: unknown,
@@ -477,20 +474,13 @@ function constantTimeDigestEqual(leftArg: string, rightArg: string): boolean {
477
474
  return left.length === right.length && plugins.crypto.timingSafeEqual(left, right);
478
475
  }
479
476
 
477
+ /**
478
+ * The document ORM reports a violated unique index as its own stable error code, so the
479
+ * insert races below recover on the declared constraint rather than on a driver error number.
480
+ */
480
481
  function isDuplicateKeyError(errorArg: unknown): boolean {
481
- return (errorArg as { code?: unknown })?.code === 11000;
482
- }
483
-
484
- function asBindingDoc(rawArg: unknown): WebPushBindingDoc {
485
- return rawArg as WebPushBindingDoc;
486
- }
487
-
488
- function asAdmissionDoc(rawArg: unknown): WebPushAdmissionDoc {
489
- return rawArg as WebPushAdmissionDoc;
490
- }
491
-
492
- function asSpoolDoc(rawArg: unknown): WebPushSpoolDoc {
493
- return rawArg as WebPushSpoolDoc;
482
+ return errorArg instanceof plugins.smartdata.SmartdataPersistenceError
483
+ && errorArg.code === 'unique_conflict';
494
484
  }
495
485
 
496
486
  function toCredentialPublic(
@@ -557,6 +547,10 @@ export function toPublicWebPushDelivery(spoolArg: WebPushSpoolDoc): IWebPushDeli
557
547
  };
558
548
  }
559
549
 
550
+ /**
551
+ * Terminal transitions scrub every encrypted and lease-scoped member in the same write that
552
+ * records the outcome. The ORM owns `_updatedAt`, so no write here touches it.
553
+ */
560
554
  export function buildWebPushTerminalUpdate(optionsArg: {
561
555
  state: Extract<
562
556
  TWebPushDeliveryState,
@@ -565,7 +559,7 @@ export function buildWebPushTerminalUpdate(optionsArg: {
565
559
  now: number;
566
560
  pushServiceStatusCode?: number;
567
561
  errorCode?: string;
568
- }): Record<string, unknown> {
562
+ }): plugins.smartdata.ISmartdataAtomicUpdate<WebPushSpoolDoc> {
569
563
  return {
570
564
  $set: {
571
565
  state: optionsArg.state,
@@ -574,7 +568,6 @@ export function buildWebPushTerminalUpdate(optionsArg: {
574
568
  purgeAt: new Date(optionsArg.now + TERMINAL_RETENTION_MS),
575
569
  pushServiceStatusCode: optionsArg.pushServiceStatusCode ?? null,
576
570
  errorCode: optionsArg.errorCode ?? null,
577
- _updatedAt: new Date(optionsArg.now).toISOString(),
578
571
  },
579
572
  $unset: {
580
573
  subscriptionEnvelope: '',
@@ -587,66 +580,26 @@ export function buildWebPushTerminalUpdate(optionsArg: {
587
580
  };
588
581
  }
589
582
 
590
- interface IWebPushNativeIndexDescription {
591
- name?: string;
592
- key?: Record<string, unknown>;
593
- unique?: boolean;
594
- expireAfterSeconds?: number;
595
- partialFilterExpression?: Record<string, unknown>;
596
- sparse?: boolean;
597
- collation?: Record<string, unknown>;
598
- hidden?: boolean;
599
- }
600
-
601
- function canonicalIndexValue(valueArg: unknown): string {
602
- if (valueArg === undefined) return 'undefined';
603
- return canonicalJson(valueArg);
604
- }
605
-
606
- function assertWebPushIndexMatches(
607
- collectionArg: TWebPushIndexCollection,
608
- actualArg: IWebPushNativeIndexDescription | undefined,
609
- expectedArg: IWebPushIndexSpecification,
610
- ): void {
611
- const malformed = (): never => {
612
- throw new Error(
613
- `Web Push persistence index ${collectionArg}.${expectedArg.name} is missing or malformed`,
614
- );
615
- };
616
- if (!actualArg) {
583
+ /**
584
+ * The three Web Push collections declare their full index topology on the model, so a divergent
585
+ * namespace is a configuration failure rather than something to repair at startup: the provider
586
+ * refuses to serve instead of delivering against an index that no longer enforces its fence.
587
+ */
588
+ async function assertWebPushCollectionTopology(
589
+ modelArg: {
590
+ inspectCollectionTopology(
591
+ dbArg: plugins.smartdata.SmartdataDb,
592
+ ): Promise<plugins.smartdata.ISmartdataCollectionTopologyInspection>;
593
+ },
594
+ dbArg: plugins.smartdata.SmartdataDb,
595
+ ): Promise<void> {
596
+ const inspection = await modelArg.inspectCollectionTopology(dbArg);
597
+ if (inspection.status !== 'exact') {
617
598
  throw new Error(
618
- `Web Push persistence index ${collectionArg}.${expectedArg.name} is missing or malformed`,
599
+ `Web Push persistence topology for ${inspection.collectionName} is ${inspection.status}`
600
+ + ` (${inspection.reasonCodes.join(', ') || 'no reason reported'})`,
619
601
  );
620
602
  }
621
- if (actualArg.name !== expectedArg.name) malformed();
622
- if (JSON.stringify(actualArg.key) !== JSON.stringify(expectedArg.key)) malformed();
623
- if (Boolean(actualArg.unique) !== Boolean(expectedArg.unique)) malformed();
624
- if (actualArg.expireAfterSeconds !== expectedArg.expireAfterSeconds) malformed();
625
- if (
626
- canonicalIndexValue(actualArg.partialFilterExpression)
627
- !== canonicalIndexValue(expectedArg.partialFilterExpression)
628
- ) {
629
- malformed();
630
- }
631
- if (
632
- actualArg.sparse === true
633
- || actualArg.collation !== undefined
634
- || actualArg.hidden === true
635
- ) {
636
- malformed();
637
- }
638
- }
639
-
640
- function assertWebPushIndexes(
641
- collectionArg: TWebPushIndexCollection,
642
- actualIndexesArg: IWebPushNativeIndexDescription[],
643
- ): void {
644
- const byName = new Map(
645
- actualIndexesArg.map((indexArg) => [indexArg.name, indexArg]),
646
- );
647
- for (const expected of getWebPushIndexSpecifications(collectionArg)) {
648
- assertWebPushIndexMatches(collectionArg, byName.get(expected.name), expected);
649
- }
650
603
  }
651
604
 
652
605
  export class WebPushManager {
@@ -715,24 +668,16 @@ export class WebPushManager {
715
668
  parseWebPushKeyRing(hmacKeyRingRaw, 'DCROUTER_WEB_PUSH_HMAC_KEY_RING'),
716
669
  );
717
670
  this.vapidSubject = validateVapidSubject(vapidSubjectRaw);
718
- const bindingCollection = WebPushBindingDoc.getNativeCollection();
719
- const admissionCollection = WebPushAdmissionDoc.getNativeCollection();
720
- const spoolCollection = WebPushSpoolDoc.getNativeCollection();
721
- await bindingCollection.findOne({}, { projection: { _id: 1 } });
722
- await admissionCollection.findOne({}, { projection: { _id: 1 } });
723
- await spoolCollection.findOne({}, { projection: { _id: 1 } });
724
- assertWebPushIndexes(
725
- 'WebPushBindingDoc',
726
- await bindingCollection.listIndexes().toArray(),
727
- );
728
- assertWebPushIndexes(
729
- 'WebPushAdmissionDoc',
730
- await admissionCollection.listIndexes().toArray(),
731
- );
732
- assertWebPushIndexes(
733
- 'WebPushSpoolDoc',
734
- await spoolCollection.listIndexes().toArray(),
735
- );
671
+ // Reading each collection proves persistence is reachable and binds the model, which
672
+ // installs any declared index the namespace is still missing; the topology assertion then
673
+ // judges what is actually there.
674
+ const db = DcRouterDb.getInstance().getDb();
675
+ await WebPushBindingDoc.exists({});
676
+ await WebPushAdmissionDoc.exists({});
677
+ await WebPushSpoolDoc.exists({});
678
+ await assertWebPushCollectionTopology(WebPushBindingDoc, db);
679
+ await assertWebPushCollectionTopology(WebPushAdmissionDoc, db);
680
+ await assertWebPushCollectionTopology(WebPushSpoolDoc, db);
736
681
  await this.runMaintenanceOnce();
737
682
  this.started = true;
738
683
  this.ready = true;
@@ -788,19 +733,18 @@ export class WebPushManager {
788
733
  'Web Push owner appInstanceId',
789
734
  );
790
735
  }
791
- const rows = await WebPushBindingDoc.getNativeCollection()
792
- .find(selector)
793
- .sort({ createdAt: 1, id: 1 })
794
- .limit(1000)
795
- .toArray();
796
- return rows.map((rowArg) => toPublicWebPushBinding(asBindingDoc(rowArg)));
736
+ const cursor = await WebPushBindingDoc.getCursor(selector, {
737
+ sort: { createdAt: 1, id: 1 },
738
+ limit: 1000,
739
+ });
740
+ const rows = await cursor.toArray();
741
+ return rows.map((rowArg) => toPublicWebPushBinding(rowArg));
797
742
  }
798
743
 
799
744
  public async getBindingById(idArg: string): Promise<WebPushBindingDoc | null> {
800
745
  this.requirePersistenceReady();
801
746
  const id = requireOpaqueId(idArg, 'Web Push binding id');
802
- const row = await WebPushBindingDoc.getNativeCollection().findOne({ id });
803
- return row ? asBindingDoc(row) : null;
747
+ return await WebPushBindingDoc.getInstance({ id });
804
748
  }
805
749
 
806
750
  public async getBindingByCredentialId(
@@ -808,13 +752,12 @@ export class WebPushManager {
808
752
  ): Promise<WebPushBindingDoc | null> {
809
753
  this.requirePersistenceReady();
810
754
  const credentialId = requireOpaqueId(credentialIdArg, 'Web Push credential id');
811
- const row = await WebPushBindingDoc.getNativeCollection().findOne({
755
+ return await WebPushBindingDoc.getInstance({
812
756
  $or: [
813
757
  { 'credential.id': credentialId },
814
758
  { 'previousCredentials.id': credentialId },
815
759
  ],
816
760
  });
817
- return row ? asBindingDoc(row) : null;
818
761
  }
819
762
 
820
763
  public async syncBinding(
@@ -843,9 +786,10 @@ export class WebPushManager {
843
786
  owner,
844
787
  enabled: bindingArg.enabled,
845
788
  });
846
- const collection = WebPushBindingDoc.getNativeCollection();
847
- const byId = requestedId ? await collection.findOne({ id: requestedId }) : null;
848
- const byOwner = await collection.findOne({
789
+ const byId = requestedId
790
+ ? await WebPushBindingDoc.getInstance({ id: requestedId })
791
+ : null;
792
+ const byOwner = await WebPushBindingDoc.getInstance({
849
793
  ownerGatewayClientType: owner.gatewayClientType,
850
794
  ownerGatewayClientId: owner.gatewayClientId,
851
795
  ownerAppInstanceId: owner.appInstanceId,
@@ -856,9 +800,8 @@ export class WebPushManager {
856
800
  if (requestedId && byOwner && String(byOwner.id) !== requestedId) {
857
801
  throw new plugins.typedrequest.TypedResponseError('Web Push binding identity conflicts with owner');
858
802
  }
859
- const existingRaw = byId || byOwner;
860
- if (existingRaw) {
861
- const existing = asBindingDoc(existingRaw);
803
+ const existing = byId || byOwner;
804
+ if (existing) {
862
805
  this.assertBindingOwner(existing, owner);
863
806
  if (
864
807
  controllerFenceDecision(existing.controllerFence, controller, requestDigest)
@@ -869,17 +812,16 @@ export class WebPushManager {
869
812
  const lifecycleGeneration = requireLifecycleGeneration(existing.lifecycleGeneration);
870
813
  if (typeof existing.deletedAt === 'number') {
871
814
  await this.scrubDeletedBindingLifecycle(existing);
872
- const scrubbedRaw = await collection.findOne({
815
+ const scrubbed = await WebPushBindingDoc.getInstance({
873
816
  id: existing.id,
874
817
  lifecycleGeneration,
875
818
  deletedAt: existing.deletedAt,
876
819
  });
877
- if (!scrubbedRaw) {
820
+ if (!scrubbed) {
878
821
  throw new plugins.typedrequest.TypedResponseError(
879
822
  'Web Push binding changed concurrently; retry synchronization',
880
823
  );
881
824
  }
882
- const scrubbed = asBindingDoc(scrubbedRaw);
883
825
  if (
884
826
  controllerFenceDecision(scrubbed.controllerFence, controller, requestDigest)
885
827
  === 'replay'
@@ -893,7 +835,7 @@ export class WebPushManager {
893
835
  const now = this.now();
894
836
  const createdCredential = this.createCredential(scrubbed.id, now);
895
837
  const nextLifecycleGeneration = lifecycleGeneration + 1;
896
- const reactivated = await collection.updateOne(
838
+ const reactivated = await WebPushBindingDoc.atomicUpdate(
897
839
  {
898
840
  id: scrubbed.id,
899
841
  mutationRevision: scrubbed.mutationRevision,
@@ -911,7 +853,6 @@ export class WebPushManager {
911
853
  lifecycleGeneration: nextLifecycleGeneration,
912
854
  controllerFence: createControllerFence(controller, requestDigest, now),
913
855
  mutationRevision: scrubbed.mutationRevision + 1,
914
- _updatedAt: new Date(now).toISOString(),
915
856
  },
916
857
  $unset: {
917
858
  deletedAt: '',
@@ -924,18 +865,18 @@ export class WebPushManager {
924
865
  'Web Push binding changed concurrently; retry synchronization',
925
866
  );
926
867
  }
927
- await WebPushAdmissionDoc.getNativeCollection().deleteMany({
868
+ await WebPushAdmissionDoc.atomicDeleteMany({
928
869
  bindingId: scrubbed.id,
929
870
  lifecycleGeneration: { $lt: nextLifecycleGeneration },
930
871
  });
931
- const refreshed = await collection.findOne({
872
+ const refreshed = await WebPushBindingDoc.getInstance({
932
873
  id: scrubbed.id,
933
874
  lifecycleGeneration: nextLifecycleGeneration,
934
875
  deletedAt: { $exists: false },
935
876
  });
936
877
  if (!refreshed) throw new Error('Reactivated Web Push binding disappeared');
937
878
  return {
938
- binding: toPublicWebPushBinding(asBindingDoc(refreshed)),
879
+ binding: toPublicWebPushBinding(refreshed),
939
880
  credential: createdCredential.oneTime,
940
881
  };
941
882
  }
@@ -945,13 +886,13 @@ export class WebPushManager {
945
886
  lifecycleGeneration,
946
887
  'BINDING_DISABLED',
947
888
  );
948
- const stillDisabled = await collection.findOne({
889
+ const stillDisabled = await WebPushBindingDoc.exists({
949
890
  id: existing.id,
950
891
  mutationRevision: existing.mutationRevision,
951
892
  lifecycleGeneration,
952
893
  enabled: false,
953
894
  deletedAt: { $exists: false },
954
- }, { projection: { _id: 1 } });
895
+ });
955
896
  if (!stillDisabled) {
956
897
  throw new plugins.typedrequest.TypedResponseError(
957
898
  'Web Push binding changed concurrently; retry synchronization',
@@ -973,10 +914,7 @@ export class WebPushManager {
973
914
  nextCredential.status = bindingArg.enabled ? 'active' : 'disabled';
974
915
  nextCredential.updatedAt = now;
975
916
  }
976
- const update: {
977
- $set: Record<string, unknown>;
978
- $unset?: Record<string, ''>;
979
- } = {
917
+ const update: plugins.smartdata.ISmartdataAtomicUpdate<WebPushBindingDoc> = {
980
918
  $set: {
981
919
  enabled: bindingArg.enabled,
982
920
  status: bindingArg.enabled ? 'active' : 'disabled',
@@ -985,7 +923,6 @@ export class WebPushManager {
985
923
  lifecycleGeneration: nextLifecycleGeneration,
986
924
  controllerFence: createControllerFence(controller, requestDigest, now),
987
925
  mutationRevision: existing.mutationRevision + 1,
988
- _updatedAt: new Date(now).toISOString(),
989
926
  ...(lifecycleChanged ? { previousCredentials: [] } : {}),
990
927
  },
991
928
  };
@@ -994,7 +931,7 @@ export class WebPushManager {
994
931
  credentialRotationRecovery: '',
995
932
  };
996
933
  }
997
- const updated = await collection.updateOne(
934
+ const updated = await WebPushBindingDoc.atomicUpdate(
998
935
  {
999
936
  id: existing.id,
1000
937
  mutationRevision: existing.mutationRevision,
@@ -1010,7 +947,7 @@ export class WebPushManager {
1010
947
  );
1011
948
  }
1012
949
  if (lifecycleChanged) {
1013
- await WebPushAdmissionDoc.getNativeCollection().deleteMany({
950
+ await WebPushAdmissionDoc.atomicDeleteMany({
1014
951
  bindingId: existing.id,
1015
952
  lifecycleGeneration: { $lt: nextLifecycleGeneration },
1016
953
  });
@@ -1022,10 +959,10 @@ export class WebPushManager {
1022
959
  bindingArg.enabled ? 'BINDING_LIFECYCLE_CHANGED' : 'BINDING_DISABLED',
1023
960
  );
1024
961
  }
1025
- const refreshed = await collection.findOne({ id: existing.id });
962
+ const refreshed = await WebPushBindingDoc.getInstance({ id: existing.id });
1026
963
  if (!refreshed) throw new Error('Synchronized Web Push binding disappeared');
1027
964
  return {
1028
- binding: toPublicWebPushBinding(asBindingDoc(refreshed)),
965
+ binding: toPublicWebPushBinding(refreshed),
1029
966
  credential,
1030
967
  };
1031
968
  }
@@ -1057,13 +994,12 @@ export class WebPushManager {
1057
994
  await doc.save();
1058
995
  } catch (error: unknown) {
1059
996
  if (!isDuplicateKeyError(error)) throw error;
1060
- const raced = await collection.findOne({
997
+ const racedBinding = await WebPushBindingDoc.getInstance({
1061
998
  ownerGatewayClientType: owner.gatewayClientType,
1062
999
  ownerGatewayClientId: owner.gatewayClientId,
1063
1000
  ownerAppInstanceId: owner.appInstanceId,
1064
1001
  });
1065
- if (!raced) throw error;
1066
- const racedBinding = asBindingDoc(raced);
1002
+ if (!racedBinding) throw error;
1067
1003
  this.assertBindingOwner(racedBinding, owner);
1068
1004
  if (
1069
1005
  controllerFenceDecision(racedBinding.controllerFence, controller, requestDigest)
@@ -1116,11 +1052,8 @@ export class WebPushManager {
1116
1052
  } else {
1117
1053
  throw new Error('Delete controller mutation cannot be replayed as synchronization');
1118
1054
  }
1119
- const refreshedRaw = await WebPushBindingDoc.getNativeCollection().findOne({
1120
- id: bindingArg.id,
1121
- });
1122
- if (!refreshedRaw) throw new Error('Synchronized Web Push binding disappeared');
1123
- const refreshed = asBindingDoc(refreshedRaw);
1055
+ const refreshed = await WebPushBindingDoc.getInstance({ id: bindingArg.id });
1056
+ if (!refreshed) throw new Error('Synchronized Web Push binding disappeared');
1124
1057
  controllerFenceDecision(refreshed.controllerFence, controllerArg, requestDigestArg);
1125
1058
  return {
1126
1059
  binding: toPublicWebPushBinding(refreshed),
@@ -1143,9 +1076,10 @@ export class WebPushManager {
1143
1076
  id: requestedId ?? null,
1144
1077
  owner,
1145
1078
  });
1146
- const collection = WebPushBindingDoc.getNativeCollection();
1147
- const byId = requestedId ? await collection.findOne({ id: requestedId }) : null;
1148
- const byOwner = await collection.findOne({
1079
+ const byId = requestedId
1080
+ ? await WebPushBindingDoc.getInstance({ id: requestedId })
1081
+ : null;
1082
+ const byOwner = await WebPushBindingDoc.getInstance({
1149
1083
  ownerGatewayClientType: owner.gatewayClientType,
1150
1084
  ownerGatewayClientId: owner.gatewayClientId,
1151
1085
  ownerAppInstanceId: owner.appInstanceId,
@@ -1160,8 +1094,8 @@ export class WebPushManager {
1160
1094
  'Web Push binding identity conflicts with owner',
1161
1095
  );
1162
1096
  }
1163
- const existingRaw = byId || byOwner;
1164
- if (!existingRaw) {
1097
+ const binding = byId || byOwner;
1098
+ if (!binding) {
1165
1099
  const now = this.now();
1166
1100
  const id = requestedId ?? plugins.uuid.v4();
1167
1101
  const tombstone = new WebPushBindingDoc();
@@ -1198,7 +1132,6 @@ export class WebPushManager {
1198
1132
  await this.scrubDeletedBindingLifecycle(tombstone);
1199
1133
  return true;
1200
1134
  }
1201
- const binding = asBindingDoc(existingRaw);
1202
1135
  this.assertBindingOwner(binding, owner);
1203
1136
  if (
1204
1137
  controllerFenceDecision(binding.controllerFence, controller, requestDigest)
@@ -1209,18 +1142,17 @@ export class WebPushManager {
1209
1142
  const lifecycleGeneration = requireLifecycleGeneration(binding.lifecycleGeneration);
1210
1143
  if (typeof binding.deletedAt === 'number') {
1211
1144
  await this.scrubDeletedBindingLifecycle(binding);
1212
- const scrubbedRaw = await collection.findOne({
1145
+ const scrubbed = await WebPushBindingDoc.getInstance({
1213
1146
  id: binding.id,
1214
1147
  lifecycleGeneration,
1215
1148
  deletedAt: binding.deletedAt,
1216
1149
  enabled: false,
1217
1150
  });
1218
- if (!scrubbedRaw) {
1151
+ if (!scrubbed) {
1219
1152
  throw new plugins.typedrequest.TypedResponseError(
1220
1153
  'Web Push binding changed concurrently; retry deletion',
1221
1154
  );
1222
1155
  }
1223
- const scrubbed = asBindingDoc(scrubbedRaw);
1224
1156
  if (
1225
1157
  controllerFenceDecision(scrubbed.controllerFence, controller, requestDigest)
1226
1158
  === 'replay'
@@ -1232,7 +1164,7 @@ export class WebPushManager {
1232
1164
  );
1233
1165
  }
1234
1166
  const now = this.now();
1235
- const recorded = await collection.updateOne(
1167
+ const recorded = await WebPushBindingDoc.atomicUpdate(
1236
1168
  {
1237
1169
  id: scrubbed.id,
1238
1170
  mutationRevision: scrubbed.mutationRevision,
@@ -1244,7 +1176,6 @@ export class WebPushManager {
1244
1176
  controllerFence: createControllerFence(controller, requestDigest, now),
1245
1177
  updatedAt: now,
1246
1178
  mutationRevision: scrubbed.mutationRevision + 1,
1247
- _updatedAt: new Date(now).toISOString(),
1248
1179
  },
1249
1180
  },
1250
1181
  );
@@ -1253,10 +1184,10 @@ export class WebPushManager {
1253
1184
  'Web Push binding changed concurrently; retry deletion',
1254
1185
  );
1255
1186
  }
1256
- const refreshedRaw = await collection.findOne({ id: scrubbed.id });
1257
- if (!refreshedRaw) throw new Error('Deleted Web Push binding tombstone disappeared');
1187
+ const refreshed = await WebPushBindingDoc.getInstance({ id: scrubbed.id });
1188
+ if (!refreshed) throw new Error('Deleted Web Push binding tombstone disappeared');
1258
1189
  return await this.resumeControllerDeleteReplay(
1259
- asBindingDoc(refreshedRaw),
1190
+ refreshed,
1260
1191
  controller,
1261
1192
  requestDigest,
1262
1193
  );
@@ -1280,7 +1211,7 @@ export class WebPushManager {
1280
1211
  return scrubbed;
1281
1212
  });
1282
1213
  const deletedLifecycleGeneration = lifecycleGeneration + 1;
1283
- const result = await WebPushBindingDoc.getNativeCollection().updateOne(
1214
+ const result = await WebPushBindingDoc.atomicUpdate(
1284
1215
  {
1285
1216
  id: binding.id,
1286
1217
  mutationRevision: binding.mutationRevision,
@@ -1299,7 +1230,6 @@ export class WebPushManager {
1299
1230
  controllerFence: createControllerFence(controller, requestDigest, now),
1300
1231
  updatedAt: now,
1301
1232
  mutationRevision: binding.mutationRevision + 1,
1302
- _updatedAt: new Date(now).toISOString(),
1303
1233
  },
1304
1234
  $unset: {
1305
1235
  credentialRotationRecovery: '',
@@ -1311,7 +1241,7 @@ export class WebPushManager {
1311
1241
  'Web Push binding changed concurrently; retry deletion',
1312
1242
  );
1313
1243
  }
1314
- const tombstone = await WebPushBindingDoc.getNativeCollection().findOne({
1244
+ const tombstone = await WebPushBindingDoc.getInstance({
1315
1245
  id: binding.id,
1316
1246
  lifecycleGeneration: deletedLifecycleGeneration,
1317
1247
  deletedAt: now,
@@ -1319,7 +1249,7 @@ export class WebPushManager {
1319
1249
  if (!tombstone) {
1320
1250
  throw new Error('Deleted Web Push binding tombstone disappeared');
1321
1251
  }
1322
- await this.scrubDeletedBindingLifecycle(asBindingDoc(tombstone));
1252
+ await this.scrubDeletedBindingLifecycle(tombstone);
1323
1253
  return true;
1324
1254
  }
1325
1255
 
@@ -1332,11 +1262,8 @@ export class WebPushManager {
1332
1262
  throw new Error('Applied Web Push delete controller mutation is inconsistent');
1333
1263
  }
1334
1264
  await this.scrubDeletedBindingLifecycle(bindingArg);
1335
- const refreshedRaw = await WebPushBindingDoc.getNativeCollection().findOne({
1336
- id: bindingArg.id,
1337
- });
1338
- if (!refreshedRaw) throw new Error('Deleted Web Push binding tombstone disappeared');
1339
- const refreshed = asBindingDoc(refreshedRaw);
1265
+ const refreshed = await WebPushBindingDoc.getInstance({ id: bindingArg.id });
1266
+ if (!refreshed) throw new Error('Deleted Web Push binding tombstone disappeared');
1340
1267
  controllerFenceDecision(refreshed.controllerFence, controllerArg, requestDigestArg);
1341
1268
  if (refreshed.enabled || typeof refreshed.deletedAt !== 'number') {
1342
1269
  throw new Error('Applied Web Push delete controller mutation is inconsistent');
@@ -1413,7 +1340,7 @@ export class WebPushManager {
1413
1340
  createdAt: now,
1414
1341
  expiresAt: now + CREDENTIAL_OVERLAP_MS,
1415
1342
  };
1416
- const result = await WebPushBindingDoc.getNativeCollection().updateOne(
1343
+ const result = await WebPushBindingDoc.atomicUpdate(
1417
1344
  {
1418
1345
  id: binding.id,
1419
1346
  mutationRevision: binding.mutationRevision,
@@ -1430,7 +1357,6 @@ export class WebPushManager {
1430
1357
  credentialRotationRecovery: recovery,
1431
1358
  updatedAt: now,
1432
1359
  mutationRevision: binding.mutationRevision + 1,
1433
- _updatedAt: new Date(now).toISOString(),
1434
1360
  },
1435
1361
  },
1436
1362
  );
@@ -1479,14 +1405,13 @@ export class WebPushManager {
1479
1405
  : keyArg
1480
1406
  ));
1481
1407
  nextKeys.push(this.createVapidKey(owner, binding.id, now));
1482
- const result = await WebPushBindingDoc.getNativeCollection().updateOne(
1408
+ const result = await WebPushBindingDoc.atomicUpdate(
1483
1409
  { id: binding.id, mutationRevision: binding.mutationRevision },
1484
1410
  {
1485
1411
  $set: {
1486
1412
  vapidKeys: nextKeys,
1487
1413
  updatedAt: now,
1488
1414
  mutationRevision: binding.mutationRevision + 1,
1489
- _updatedAt: new Date(now).toISOString(),
1490
1415
  },
1491
1416
  },
1492
1417
  );
@@ -1647,8 +1572,7 @@ export class WebPushManager {
1647
1572
  canonicalRequest,
1648
1573
  hmacKeyId,
1649
1574
  ).digest;
1650
- const collection = WebPushSpoolDoc.getNativeCollection();
1651
- const existing = await collection.findOne({
1575
+ const existing = await WebPushSpoolDoc.getInstance({
1652
1576
  bindingId: context.binding.id,
1653
1577
  lifecycleGeneration,
1654
1578
  credentialId: context.credential.id,
@@ -1656,24 +1580,23 @@ export class WebPushManager {
1656
1580
  });
1657
1581
  if (existing) {
1658
1582
  await this.assertBindingLifecycleActive(context.binding);
1659
- const spool = asSpoolDoc(existing);
1660
- if (!constantTimeDigestEqual(spool.requestDigest, requestDigest)) {
1583
+ if (!constantTimeDigestEqual(existing.requestDigest, requestDigest)) {
1661
1584
  throw new plugins.typedrequest.TypedResponseError(
1662
1585
  'Web Push idempotency key conflicts with a different request',
1663
1586
  );
1664
1587
  }
1665
1588
  return {
1666
1589
  accepted: true,
1667
- spoolItemId: spool.id,
1590
+ spoolItemId: existing.id,
1668
1591
  };
1669
1592
  }
1670
1593
  await this.assertBindingLifecycleActive(context.binding);
1671
1594
  await this.reserveAdmission(context.binding);
1672
- const nonterminalCount = await collection.countDocuments({
1595
+ const nonterminalCount = await WebPushSpoolDoc.getCount({
1673
1596
  bindingId: context.binding.id,
1674
1597
  lifecycleGeneration,
1675
1598
  state: { $in: NONTERMINAL_STATES },
1676
- });
1599
+ }, { limit: MAX_NONTERMINAL_ITEMS_PER_BINDING });
1677
1600
  if (nonterminalCount >= MAX_NONTERMINAL_ITEMS_PER_BINDING) {
1678
1601
  throw new plugins.typedrequest.TypedResponseError('Web Push binding queue quota exceeded');
1679
1602
  }
@@ -1730,14 +1653,13 @@ export class WebPushManager {
1730
1653
  await spool.save();
1731
1654
  } catch (error: unknown) {
1732
1655
  if (!isDuplicateKeyError(error)) throw error;
1733
- const raced = await collection.findOne({
1656
+ const racedSpool = await WebPushSpoolDoc.getInstance({
1734
1657
  bindingId: context.binding.id,
1735
1658
  lifecycleGeneration,
1736
1659
  credentialId: context.credential.id,
1737
1660
  idempotencyKeyDigest,
1738
1661
  });
1739
- if (!raced) throw error;
1740
- const racedSpool = asSpoolDoc(raced);
1662
+ if (!racedSpool) throw error;
1741
1663
  if (!constantTimeDigestEqual(racedSpool.requestDigest, requestDigest)) {
1742
1664
  throw new plugins.typedrequest.TypedResponseError(
1743
1665
  'Web Push idempotency key conflicts with a different request',
@@ -1773,12 +1695,12 @@ export class WebPushManager {
1773
1695
  const lifecycleGeneration = requireLifecycleGeneration(
1774
1696
  context.binding.lifecycleGeneration,
1775
1697
  );
1776
- const row = await WebPushSpoolDoc.getNativeCollection().findOne({
1698
+ const row = await WebPushSpoolDoc.getInstance({
1777
1699
  id: spoolItemId,
1778
1700
  bindingId: context.binding.id,
1779
1701
  lifecycleGeneration,
1780
1702
  });
1781
- return row ? toPublicWebPushDelivery(asSpoolDoc(row)) : undefined;
1703
+ return row ? toPublicWebPushDelivery(row) : undefined;
1782
1704
  }
1783
1705
 
1784
1706
  public async cancel(
@@ -2015,13 +1937,13 @@ export class WebPushManager {
2015
1937
 
2016
1938
  private async assertBindingLifecycleActive(bindingArg: WebPushBindingDoc): Promise<void> {
2017
1939
  const lifecycleGeneration = requireLifecycleGeneration(bindingArg.lifecycleGeneration);
2018
- const active = await WebPushBindingDoc.getNativeCollection().findOne({
1940
+ const active = await WebPushBindingDoc.exists({
2019
1941
  id: bindingArg.id,
2020
1942
  lifecycleGeneration,
2021
1943
  enabled: true,
2022
1944
  status: 'active',
2023
1945
  deletedAt: { $exists: false },
2024
- }, { projection: { _id: 1 } });
1946
+ });
2025
1947
  if (!active) {
2026
1948
  throw new plugins.typedrequest.TypedResponseError(
2027
1949
  'Web Push binding lifecycle changed; retry with current credentials',
@@ -2033,17 +1955,16 @@ export class WebPushManager {
2033
1955
  const bindingId = requireOpaqueId(bindingArg.id, 'Web Push binding id');
2034
1956
  const lifecycleGeneration = requireLifecycleGeneration(bindingArg.lifecycleGeneration);
2035
1957
  await this.assertBindingLifecycleActive(bindingArg);
2036
- const collection = WebPushAdmissionDoc.getNativeCollection();
2037
1958
  const policy = this.admissionPolicy;
2038
1959
 
2039
1960
  for (let attempt = 0; attempt < MAX_ADMISSION_CAS_ATTEMPTS; attempt++) {
2040
1961
  const now = this.now();
2041
1962
  const purgeAt = new Date(now + policy.longWindowMs * 2);
2042
- const raw = await collection.findOne({
1963
+ const current = await WebPushAdmissionDoc.getInstance({
2043
1964
  bindingId,
2044
1965
  lifecycleGeneration,
2045
1966
  });
2046
- if (!raw) {
1967
+ if (!current) {
2047
1968
  const admission = new WebPushAdmissionDoc();
2048
1969
  admission.bindingId = bindingId;
2049
1970
  admission.lifecycleGeneration = lifecycleGeneration;
@@ -2063,7 +1984,7 @@ export class WebPushManager {
2063
1984
  }
2064
1985
  }
2065
1986
 
2066
- const admission = asAdmissionDoc(raw);
1987
+ const admission = current;
2067
1988
  const admissionGeneration = requireLifecycleGeneration(admission.lifecycleGeneration);
2068
1989
  if (admissionGeneration !== lifecycleGeneration) {
2069
1990
  throw new Error('Web Push admission lifecycle generation is inconsistent');
@@ -2096,7 +2017,7 @@ export class WebPushManager {
2096
2017
  );
2097
2018
  }
2098
2019
 
2099
- const updated = await collection.updateOne(
2020
+ const updated = await WebPushAdmissionDoc.atomicUpdate(
2100
2021
  {
2101
2022
  bindingId,
2102
2023
  lifecycleGeneration,
@@ -2111,7 +2032,6 @@ export class WebPushManager {
2111
2032
  revision: admission.revision + 1,
2112
2033
  updatedAt: now,
2113
2034
  purgeAt,
2114
- _updatedAt: new Date(now).toISOString(),
2115
2035
  },
2116
2036
  },
2117
2037
  );
@@ -2129,13 +2049,13 @@ export class WebPushManager {
2129
2049
  lifecycleGenerationArg: number,
2130
2050
  ): Promise<void> {
2131
2051
  const lifecycleGeneration = requireLifecycleGeneration(lifecycleGenerationArg);
2132
- const active = await WebPushBindingDoc.getNativeCollection().findOne({
2052
+ const active = await WebPushBindingDoc.exists({
2133
2053
  id: bindingIdArg,
2134
2054
  lifecycleGeneration,
2135
2055
  enabled: true,
2136
2056
  status: 'active',
2137
2057
  deletedAt: { $exists: false },
2138
- }, { projection: { _id: 1 } });
2058
+ });
2139
2059
  if (active) return;
2140
2060
  await this.cancelWithinBinding(
2141
2061
  bindingIdArg,
@@ -2147,7 +2067,9 @@ export class WebPushManager {
2147
2067
  lifecycleGeneration,
2148
2068
  'BINDING_LIFECYCLE_CHANGED',
2149
2069
  );
2150
- await WebPushAdmissionDoc.getNativeCollection().deleteOne({
2070
+ // `{ bindingId, lifecycleGeneration }` is the collection's declared unique key, so this
2071
+ // deletes exactly the one admission counter that belongs to the drained lifecycle.
2072
+ await WebPushAdmissionDoc.atomicDelete({
2151
2073
  bindingId: bindingIdArg,
2152
2074
  lifecycleGeneration,
2153
2075
  });
@@ -2167,17 +2089,15 @@ export class WebPushManager {
2167
2089
  tombstoneGeneration,
2168
2090
  cancellationErrorCode,
2169
2091
  );
2170
- const collection = WebPushBindingDoc.getNativeCollection();
2171
2092
  let scrubbed = false;
2172
2093
  for (let attempt = 0; attempt < 4; attempt++) {
2173
- const currentRaw = await collection.findOne({
2094
+ const current = await WebPushBindingDoc.getInstance({
2174
2095
  id: bindingArg.id,
2175
2096
  lifecycleGeneration: tombstoneGeneration,
2176
2097
  deletedAt: bindingArg.deletedAt,
2177
2098
  enabled: false,
2178
2099
  });
2179
- if (!currentRaw) return;
2180
- const current = asBindingDoc(currentRaw);
2100
+ if (!current) return;
2181
2101
  const needsScrub = (
2182
2102
  typeof current.credential?.secretHash === 'string'
2183
2103
  || (current.previousCredentials || []).length > 0
@@ -2210,7 +2130,7 @@ export class WebPushManager {
2210
2130
  delete scrubbedKey.privateKeyEnvelope;
2211
2131
  return scrubbedKey;
2212
2132
  });
2213
- const result = await collection.updateOne(
2133
+ const result = await WebPushBindingDoc.atomicUpdate(
2214
2134
  {
2215
2135
  id: current.id,
2216
2136
  mutationRevision: current.mutationRevision,
@@ -2225,7 +2145,6 @@ export class WebPushManager {
2225
2145
  vapidKeys,
2226
2146
  updatedAt: now,
2227
2147
  mutationRevision: current.mutationRevision + 1,
2228
- _updatedAt: new Date(now).toISOString(),
2229
2148
  },
2230
2149
  $unset: {
2231
2150
  credentialRotationRecovery: '',
@@ -2242,7 +2161,7 @@ export class WebPushManager {
2242
2161
  'Web Push binding cleanup changed concurrently; retry deletion',
2243
2162
  );
2244
2163
  }
2245
- await WebPushAdmissionDoc.getNativeCollection().deleteMany({
2164
+ await WebPushAdmissionDoc.atomicDeleteMany({
2246
2165
  bindingId: bindingArg.id,
2247
2166
  lifecycleGeneration: { $lte: tombstoneGeneration },
2248
2167
  });
@@ -2264,7 +2183,6 @@ export class WebPushManager {
2264
2183
  limitArg = MAINTENANCE_BATCH_SIZE,
2265
2184
  ): Promise<void> {
2266
2185
  const now = this.now();
2267
- const collection = WebPushBindingDoc.getNativeCollection();
2268
2186
  const batch = await this.selectBindingMaintenanceBatch(
2269
2187
  { 'previousCredentials.validUntil': { $lte: now } },
2270
2188
  this.expiredCredentialOverlapCursor,
@@ -2280,7 +2198,7 @@ export class WebPushManager {
2280
2198
  ),
2281
2199
  );
2282
2200
  if (remaining.length === (binding.previousCredentials || []).length) continue;
2283
- await collection.updateOne(
2201
+ await WebPushBindingDoc.atomicUpdate(
2284
2202
  {
2285
2203
  id: binding.id,
2286
2204
  mutationRevision: binding.mutationRevision,
@@ -2291,7 +2209,6 @@ export class WebPushManager {
2291
2209
  previousCredentials: remaining,
2292
2210
  updatedAt: now,
2293
2211
  mutationRevision: binding.mutationRevision + 1,
2294
- _updatedAt: new Date(now).toISOString(),
2295
2212
  },
2296
2213
  },
2297
2214
  );
@@ -2302,7 +2219,6 @@ export class WebPushManager {
2302
2219
  limitArg = MAINTENANCE_BATCH_SIZE,
2303
2220
  ): Promise<void> {
2304
2221
  const now = this.now();
2305
- const collection = WebPushBindingDoc.getNativeCollection();
2306
2222
  const batch = await this.selectBindingMaintenanceBatch(
2307
2223
  { 'credentialRotationRecovery.expiresAt': { $lte: now } },
2308
2224
  this.expiredCredentialRecoveryCursor,
@@ -2312,7 +2228,7 @@ export class WebPushManager {
2312
2228
  for (const binding of batch.rows) {
2313
2229
  const recovery = binding.credentialRotationRecovery;
2314
2230
  if (!recovery || recovery.expiresAt > now) continue;
2315
- await collection.updateOne(
2231
+ await WebPushBindingDoc.atomicUpdate(
2316
2232
  {
2317
2233
  id: binding.id,
2318
2234
  mutationRevision: binding.mutationRevision,
@@ -2327,7 +2243,6 @@ export class WebPushManager {
2327
2243
  $set: {
2328
2244
  updatedAt: now,
2329
2245
  mutationRevision: binding.mutationRevision + 1,
2330
- _updatedAt: new Date(now).toISOString(),
2331
2246
  },
2332
2247
  $unset: {
2333
2248
  credentialRotationRecovery: '',
@@ -2342,8 +2257,6 @@ export class WebPushManager {
2342
2257
  limitArg = MAINTENANCE_BATCH_SIZE,
2343
2258
  ): Promise<void> {
2344
2259
  const now = this.now();
2345
- const bindingCollection = WebPushBindingDoc.getNativeCollection();
2346
- const spoolCollection = WebPushSpoolDoc.getNativeCollection();
2347
2260
  const selector: Record<string, unknown> = bindingIdArg
2348
2261
  ? { id: requireOpaqueId(bindingIdArg, 'Web Push binding id') }
2349
2262
  : {
@@ -2353,11 +2266,7 @@ export class WebPushManager {
2353
2266
  ],
2354
2267
  };
2355
2268
  const rows = bindingIdArg
2356
- ? (await bindingCollection
2357
- .find(selector)
2358
- .limit(1)
2359
- .toArray())
2360
- .map((rawArg) => asBindingDoc(rawArg))
2269
+ ? await (await WebPushBindingDoc.getCursor(selector, { limit: 1 })).toArray()
2361
2270
  : await (async () => {
2362
2271
  const batch = await this.selectBindingMaintenanceBatch(
2363
2272
  selector,
@@ -2382,7 +2291,7 @@ export class WebPushManager {
2382
2291
  continue;
2383
2292
  }
2384
2293
  if (key.status === 'retired') continue;
2385
- const pending = await spoolCollection.countDocuments({
2294
+ const pending = await WebPushSpoolDoc.getCount({
2386
2295
  bindingId: binding.id,
2387
2296
  lifecycleGeneration: requireLifecycleGeneration(binding.lifecycleGeneration),
2388
2297
  vapidKeyId: key.id,
@@ -2393,7 +2302,7 @@ export class WebPushManager {
2393
2302
  }
2394
2303
  }
2395
2304
  if (retained.length === binding.vapidKeys.length) continue;
2396
- await bindingCollection.updateOne(
2305
+ await WebPushBindingDoc.atomicUpdate(
2397
2306
  {
2398
2307
  id: binding.id,
2399
2308
  mutationRevision: binding.mutationRevision,
@@ -2404,7 +2313,6 @@ export class WebPushManager {
2404
2313
  vapidKeys: retained,
2405
2314
  updatedAt: now,
2406
2315
  mutationRevision: binding.mutationRevision + 1,
2407
- _updatedAt: new Date(now).toISOString(),
2408
2316
  },
2409
2317
  },
2410
2318
  );
@@ -2446,7 +2354,7 @@ export class WebPushManager {
2446
2354
  lifecycleGeneration,
2447
2355
  'BINDING_DISABLED',
2448
2356
  );
2449
- await WebPushAdmissionDoc.getNativeCollection().deleteMany({
2357
+ await WebPushAdmissionDoc.atomicDeleteMany({
2450
2358
  bindingId: binding.id,
2451
2359
  lifecycleGeneration: { $lt: lifecycleGeneration },
2452
2360
  });
@@ -2481,12 +2389,11 @@ export class WebPushManager {
2481
2389
  { id: { $gt: cursorArg } },
2482
2390
  ],
2483
2391
  };
2484
- const rows = (await WebPushBindingDoc.getNativeCollection()
2485
- .find(selector)
2486
- .sort({ id: 1 })
2487
- .limit(Math.min(Math.max(Math.floor(limitArg), 1), MAINTENANCE_BATCH_SIZE))
2488
- .toArray())
2489
- .map((rawArg) => asBindingDoc(rawArg));
2392
+ const cursor = await WebPushBindingDoc.getCursor(selector, {
2393
+ sort: { id: 1 },
2394
+ limit: Math.min(Math.max(Math.floor(limitArg), 1), MAINTENANCE_BATCH_SIZE),
2395
+ });
2396
+ const rows = await cursor.toArray();
2490
2397
  return {
2491
2398
  rows,
2492
2399
  ...(rows.length > 0 ? { nextCursor: rows[rows.length - 1].id } : {}),
@@ -2526,7 +2433,7 @@ export class WebPushManager {
2526
2433
  beforeLifecycleGeneration,
2527
2434
  errorCodeArg,
2528
2435
  );
2529
- await WebPushAdmissionDoc.getNativeCollection().deleteMany({
2436
+ await WebPushAdmissionDoc.atomicDeleteMany({
2530
2437
  bindingId: bindingIdArg,
2531
2438
  lifecycleGeneration: { $lt: beforeLifecycleGeneration },
2532
2439
  });
@@ -2547,7 +2454,6 @@ export class WebPushManager {
2547
2454
  beforeLifecycleGenerationArg,
2548
2455
  );
2549
2456
  const deadline = Date.now() + LIFECYCLE_DRAIN_TIMEOUT_MS;
2550
- const collection = WebPushSpoolDoc.getNativeCollection();
2551
2457
  while (true) {
2552
2458
  await this.requestBindingLifecyclesBeforeCancellation(
2553
2459
  bindingId,
@@ -2563,7 +2469,7 @@ export class WebPushManager {
2563
2469
  ) {
2564
2470
  // Drain every bounded stale-lease batch before checking completion.
2565
2471
  }
2566
- const remaining = await collection.countDocuments({
2472
+ const remaining = await WebPushSpoolDoc.getCount({
2567
2473
  bindingId,
2568
2474
  lifecycleGeneration: { $lt: beforeLifecycleGeneration },
2569
2475
  state: { $in: NONTERMINAL_STATES },
@@ -2584,8 +2490,7 @@ export class WebPushManager {
2584
2490
  limitArg = MAINTENANCE_BATCH_SIZE,
2585
2491
  ): Promise<number> {
2586
2492
  const now = this.now();
2587
- const collection = WebPushSpoolDoc.getNativeCollection();
2588
- const rows = await collection.find({
2493
+ const cursor = await WebPushSpoolDoc.getCursor({
2589
2494
  state: 'sending',
2590
2495
  cancelRequestedAt: { $exists: true },
2591
2496
  $or: [
@@ -2600,14 +2505,13 @@ export class WebPushManager {
2600
2505
  leaseExpiresAt: 1,
2601
2506
  cancelRequestedAt: 1,
2602
2507
  },
2603
- })
2604
- .sort({ cancelRequestedAt: 1, id: 1 })
2605
- .limit(Math.min(Math.max(Math.floor(limitArg), 1), MAINTENANCE_BATCH_SIZE))
2606
- .toArray();
2508
+ sort: { cancelRequestedAt: 1, id: 1 },
2509
+ limit: Math.min(Math.max(Math.floor(limitArg), 1), MAINTENANCE_BATCH_SIZE),
2510
+ });
2511
+ const rows = await cursor.toArray();
2607
2512
  let finished = 0;
2608
- for (const raw of rows) {
2609
- const spool = asSpoolDoc(raw);
2610
- const result = await collection.updateOne(
2513
+ for (const spool of rows) {
2514
+ const result = await WebPushSpoolDoc.atomicUpdate(
2611
2515
  {
2612
2516
  id: spool.id,
2613
2517
  state: 'sending',
@@ -2640,8 +2544,7 @@ export class WebPushManager {
2640
2544
  beforeLifecycleGenerationArg,
2641
2545
  );
2642
2546
  const now = this.now();
2643
- const collection = WebPushSpoolDoc.getNativeCollection();
2644
- const rows = await collection.find({
2547
+ const cursor = await WebPushSpoolDoc.getCursor({
2645
2548
  bindingId,
2646
2549
  lifecycleGeneration: { $lt: beforeLifecycleGeneration },
2647
2550
  state: 'sending',
@@ -2659,11 +2562,12 @@ export class WebPushManager {
2659
2562
  cancelRequestedAt: 1,
2660
2563
  cancelErrorCode: 1,
2661
2564
  },
2662
- }).limit(MAINTENANCE_BATCH_SIZE).toArray();
2565
+ limit: MAINTENANCE_BATCH_SIZE,
2566
+ });
2567
+ const rows = await cursor.toArray();
2663
2568
  let finished = 0;
2664
- for (const raw of rows) {
2665
- const spool = asSpoolDoc(raw);
2666
- const result = await collection.updateOne(
2569
+ for (const spool of rows) {
2570
+ const result = await WebPushSpoolDoc.atomicUpdate(
2667
2571
  {
2668
2572
  id: spool.id,
2669
2573
  bindingId,
@@ -2706,7 +2610,6 @@ export class WebPushManager {
2706
2610
  'Web Push subscriptionId',
2707
2611
  );
2708
2612
  }
2709
- const collection = WebPushSpoolDoc.getNativeCollection();
2710
2613
  const lifecycleGenerationSelector = typeof lifecycleGenerationArg === 'number'
2711
2614
  ? requireLifecycleGeneration(lifecycleGenerationArg)
2712
2615
  : lifecycleGenerationArg === undefined
@@ -2719,14 +2622,14 @@ export class WebPushManager {
2719
2622
  : { lifecycleGeneration: lifecycleGenerationSelector }),
2720
2623
  ...targetSelector,
2721
2624
  };
2722
- const alreadyTerminalCount = await collection.countDocuments({
2625
+ const alreadyTerminalCount = await WebPushSpoolDoc.getCount({
2723
2626
  ...selector,
2724
2627
  state: { $in: TERMINAL_STATES },
2725
2628
  });
2726
2629
  let cancelledCount = 0;
2727
2630
  for (let attempt = 0; attempt < 16; attempt++) {
2728
2631
  const now = this.now();
2729
- const terminalized = await collection.updateMany(
2632
+ const terminalized = await WebPushSpoolDoc.atomicUpdateMany(
2730
2633
  {
2731
2634
  ...selector,
2732
2635
  state: { $in: ['accepted', 'queued', 'deferred'] },
@@ -2737,7 +2640,7 @@ export class WebPushManager {
2737
2640
  errorCode: errorCodeArg,
2738
2641
  }),
2739
2642
  );
2740
- const markedSending = await collection.updateMany(
2643
+ const markedSending = await WebPushSpoolDoc.atomicUpdateMany(
2741
2644
  {
2742
2645
  ...selector,
2743
2646
  state: 'sending',
@@ -2748,7 +2651,6 @@ export class WebPushManager {
2748
2651
  cancelRequestedAt: now,
2749
2652
  cancelErrorCode: errorCodeArg,
2750
2653
  updatedAt: now,
2751
- _updatedAt: new Date(now).toISOString(),
2752
2654
  },
2753
2655
  },
2754
2656
  );
@@ -2764,18 +2666,15 @@ export class WebPushManager {
2764
2666
 
2765
2667
  private async expireDueItems(limitArg: number): Promise<void> {
2766
2668
  const now = this.now();
2767
- const collection = WebPushSpoolDoc.getNativeCollection();
2768
- const rows = await collection
2769
- .find({
2770
- state: { $in: NONTERMINAL_STATES },
2771
- expiresAt: { $lte: now },
2772
- }, {
2773
- projection: { id: 1, state: 1, leaseToken: 1, leaseExpiresAt: 1 },
2774
- })
2775
- .limit(limitArg)
2776
- .toArray();
2777
- for (const raw of rows) {
2778
- const spool = asSpoolDoc(raw);
2669
+ const cursor = await WebPushSpoolDoc.getCursor({
2670
+ state: { $in: NONTERMINAL_STATES },
2671
+ expiresAt: { $lte: now },
2672
+ }, {
2673
+ projection: { id: 1, state: 1, leaseToken: 1, leaseExpiresAt: 1 },
2674
+ limit: limitArg,
2675
+ });
2676
+ const rows = await cursor.toArray();
2677
+ for (const spool of rows) {
2779
2678
  if (
2780
2679
  spool.state === 'sending'
2781
2680
  && typeof spool.leaseExpiresAt === 'number'
@@ -2793,7 +2692,7 @@ export class WebPushManager {
2793
2692
  : { leaseExpiresAt: spool.leaseExpiresAt }),
2794
2693
  }
2795
2694
  : {};
2796
- await collection.updateOne(
2695
+ await WebPushSpoolDoc.atomicUpdate(
2797
2696
  {
2798
2697
  id: spool.id,
2799
2698
  state: spool.state,
@@ -2811,21 +2710,25 @@ export class WebPushManager {
2811
2710
  private async claimNextItem(): Promise<WebPushSpoolDoc | null> {
2812
2711
  const now = this.now();
2813
2712
  const leaseToken = plugins.uuid.v4();
2814
- const row = await WebPushSpoolDoc.getNativeCollection().findOneAndUpdate(
2713
+ // One claimable state per `$or` branch: the ORM requires every branch of a write filter to
2714
+ // carry a top-level equality anchor, and `state: { $in: [...] }` anchors nothing. The three
2715
+ // branches match exactly the same documents as the single `$in` did and still ride
2716
+ // `web_push_spool_delivery_scan`.
2717
+ const claimed = await WebPushSpoolDoc.atomicFindOneAndUpdate(
2815
2718
  {
2816
2719
  expiresAt: { $gt: now },
2817
2720
  $or: [
2818
- {
2819
- state: { $in: ['accepted', 'queued', 'deferred'] },
2721
+ ...CLAIMABLE_IDLE_STATES.map((stateArg) => ({
2722
+ state: stateArg,
2820
2723
  nextAttemptAt: { $lte: now },
2821
2724
  cancelRequestedAt: { $exists: false },
2822
2725
  $or: [
2823
2726
  { leaseToken: { $exists: false } },
2824
2727
  { leaseExpiresAt: { $lte: now } },
2825
2728
  ],
2826
- },
2729
+ })),
2827
2730
  {
2828
- state: 'sending',
2731
+ state: 'sending' as const,
2829
2732
  leaseExpiresAt: { $lte: now },
2830
2733
  cancelRequestedAt: { $exists: false },
2831
2734
  },
@@ -2837,7 +2740,6 @@ export class WebPushManager {
2837
2740
  leaseToken,
2838
2741
  leaseExpiresAt: now + DELIVERY_LEASE_MS,
2839
2742
  updatedAt: now,
2840
- _updatedAt: new Date(now).toISOString(),
2841
2743
  },
2842
2744
  $inc: { attempts: 1 },
2843
2745
  },
@@ -2846,21 +2748,19 @@ export class WebPushManager {
2846
2748
  returnDocument: 'after',
2847
2749
  },
2848
2750
  );
2849
- return row ? asSpoolDoc(row) : null;
2751
+ return claimed.status === 'matched' ? claimed.document : null;
2850
2752
  }
2851
2753
 
2852
2754
  private async processClaimedItem(spoolArg: WebPushSpoolDoc): Promise<void> {
2853
- const collection = WebPushSpoolDoc.getNativeCollection();
2854
2755
  const leaseToken = spoolArg.leaseToken;
2855
2756
  if (!leaseToken) return;
2856
2757
  const now = this.now();
2857
- const current = await collection.findOne({
2758
+ const currentSpool = await WebPushSpoolDoc.getInstance({
2858
2759
  id: spoolArg.id,
2859
2760
  state: 'sending',
2860
2761
  leaseToken,
2861
2762
  });
2862
- if (!current) return;
2863
- const currentSpool = asSpoolDoc(current);
2763
+ if (!currentSpool) return;
2864
2764
  if (currentSpool.cancelRequestedAt !== undefined) {
2865
2765
  await this.finishTerminal(currentSpool, 'cancelled', {
2866
2766
  errorCode: currentSpool.cancelErrorCode || 'CANCELLED_BY_APPLICATION',
@@ -2882,20 +2782,19 @@ export class WebPushManager {
2882
2782
  });
2883
2783
  return;
2884
2784
  }
2885
- const bindingRaw = await WebPushBindingDoc.getNativeCollection().findOne({
2785
+ const binding = await WebPushBindingDoc.getInstance({
2886
2786
  id: currentSpool.bindingId,
2887
2787
  lifecycleGeneration,
2888
2788
  enabled: true,
2889
2789
  status: 'active',
2890
2790
  deletedAt: { $exists: false },
2891
2791
  });
2892
- if (!bindingRaw) {
2792
+ if (!binding) {
2893
2793
  await this.finishTerminal(currentSpool, 'cancelled', {
2894
2794
  errorCode: 'BINDING_LIFECYCLE_CHANGED',
2895
2795
  });
2896
2796
  return;
2897
2797
  }
2898
- const binding = asBindingDoc(bindingRaw);
2899
2798
  const vapidKey = binding.vapidKeys.find((keyArg) => (
2900
2799
  keyArg.id === currentSpool.vapidKeyId
2901
2800
  && Boolean(keyArg.privateKeyEnvelope)
@@ -2944,26 +2843,25 @@ export class WebPushManager {
2944
2843
  });
2945
2844
  return;
2946
2845
  }
2947
- const immediatelyBeforeSend = await collection.findOne({
2846
+ const immediatelyBeforeSendSpool = await WebPushSpoolDoc.getInstance({
2948
2847
  id: currentSpool.id,
2949
2848
  state: 'sending',
2950
2849
  leaseToken,
2951
2850
  });
2952
- if (!immediatelyBeforeSend) return;
2953
- const immediatelyBeforeSendSpool = asSpoolDoc(immediatelyBeforeSend);
2851
+ if (!immediatelyBeforeSendSpool) return;
2954
2852
  if (immediatelyBeforeSendSpool.cancelRequestedAt !== undefined) {
2955
2853
  await this.finishTerminal(immediatelyBeforeSendSpool, 'cancelled', {
2956
2854
  errorCode: immediatelyBeforeSendSpool.cancelErrorCode || 'CANCELLED_BY_APPLICATION',
2957
2855
  });
2958
2856
  return;
2959
2857
  }
2960
- const bindingImmediatelyBeforeSend = await WebPushBindingDoc.getNativeCollection().findOne({
2858
+ const bindingImmediatelyBeforeSend = await WebPushBindingDoc.exists({
2961
2859
  id: currentSpool.bindingId,
2962
2860
  lifecycleGeneration,
2963
2861
  enabled: true,
2964
2862
  status: 'active',
2965
2863
  deletedAt: { $exists: false },
2966
- }, { projection: { _id: 1 } });
2864
+ });
2967
2865
  if (!bindingImmediatelyBeforeSend) {
2968
2866
  await this.finishTerminal(currentSpool, 'cancelled', {
2969
2867
  errorCode: 'BINDING_LIFECYCLE_CHANGED',
@@ -3058,7 +2956,7 @@ export class WebPushManager {
3058
2956
  ): Promise<void> {
3059
2957
  if (!spoolArg.leaseToken) return;
3060
2958
  const now = this.now();
3061
- await WebPushSpoolDoc.getNativeCollection().updateOne(
2959
+ await WebPushSpoolDoc.atomicUpdate(
3062
2960
  {
3063
2961
  id: spoolArg.id,
3064
2962
  state: 'sending',
@@ -3081,14 +2979,12 @@ export class WebPushManager {
3081
2979
  },
3082
2980
  ): Promise<void> {
3083
2981
  if (!spoolArg.leaseToken) return;
3084
- const collection = WebPushSpoolDoc.getNativeCollection();
3085
- const currentRaw = await collection.findOne({
2982
+ const current = await WebPushSpoolDoc.getInstance({
3086
2983
  id: spoolArg.id,
3087
2984
  state: 'sending',
3088
2985
  leaseToken: spoolArg.leaseToken,
3089
2986
  });
3090
- if (!currentRaw) return;
3091
- const current = asSpoolDoc(currentRaw);
2987
+ if (!current) return;
3092
2988
  if (current.cancelRequestedAt !== undefined) {
3093
2989
  await this.finishTerminal(current, 'cancelled', {
3094
2990
  errorCode: current.cancelErrorCode || 'CANCELLED_BY_APPLICATION',
@@ -3111,7 +3007,7 @@ export class WebPushManager {
3111
3007
  );
3112
3008
  return;
3113
3009
  }
3114
- await collection.updateOne(
3010
+ await WebPushSpoolDoc.atomicUpdate(
3115
3011
  {
3116
3012
  id: current.id,
3117
3013
  state: 'sending',
@@ -3125,7 +3021,6 @@ export class WebPushManager {
3125
3021
  updatedAt: now,
3126
3022
  pushServiceStatusCode: metadataArg.pushServiceStatusCode ?? null,
3127
3023
  errorCode: metadataArg.errorCode,
3128
- _updatedAt: new Date(now).toISOString(),
3129
3024
  },
3130
3025
  $unset: {
3131
3026
  leaseToken: '',