@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.
- package/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +2 -2
- package/dist_ts/config/classes.gateway-client-manager.js +1 -1
- package/dist_ts/db/documents/classes.cached.email.d.ts +0 -1
- package/dist_ts/db/documents/classes.cached.email.js +45 -48
- package/dist_ts/db/documents/classes.email-traffic-bucket.doc.d.ts +0 -1
- package/dist_ts/db/documents/classes.email-traffic-bucket.doc.js +61 -50
- package/dist_ts/db/documents/classes.gateway-client.doc.d.ts +9 -2
- package/dist_ts/db/documents/classes.gateway-client.doc.js +9 -11
- package/dist_ts/db/documents/classes.ip-intelligence.doc.d.ts +4 -3
- package/dist_ts/db/documents/classes.ip-intelligence.doc.js +27 -31
- package/dist_ts/db/documents/classes.ops-config-event.doc.d.ts +0 -2
- package/dist_ts/db/documents/classes.ops-config-event.doc.js +70 -68
- package/dist_ts/db/documents/classes.smartmta-storage.doc.d.ts +14 -0
- package/dist_ts/db/documents/classes.smartmta-storage.doc.js +17 -3
- package/dist_ts/db/documents/classes.webpush-admission.doc.d.ts +0 -1
- package/dist_ts/db/documents/classes.webpush-admission.doc.js +14 -5
- package/dist_ts/db/documents/classes.webpush-binding.doc.d.ts +1 -1
- package/dist_ts/db/documents/classes.webpush-binding.doc.js +46 -8
- package/dist_ts/db/documents/classes.webpush-spool.doc.d.ts +0 -1
- package/dist_ts/db/documents/classes.webpush-spool.doc.js +34 -6
- package/dist_ts/dns/manager.dns.js +7 -3
- package/dist_ts/email/classes.smartmta-storage-manager.d.ts +15 -3
- package/dist_ts/email/classes.smartmta-storage-manager.js +62 -36
- package/dist_ts/security/classes.security-policy-manager.js +11 -9
- package/dist_ts/webpush/classes.webpush-manager.d.ts +5 -1
- package/dist_ts/webpush/classes.webpush-manager.js +162 -232
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +3 -3
- package/readme.md +1 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +1 -1
- package/ts/config/classes.gateway-client-manager.ts +4 -2
- package/ts/db/documents/classes.cached.email.ts +51 -66
- package/ts/db/documents/classes.email-traffic-bucket.doc.ts +65 -59
- package/ts/db/documents/classes.gateway-client.doc.ts +38 -18
- package/ts/db/documents/classes.ip-intelligence.doc.ts +37 -40
- package/ts/db/documents/classes.ops-config-event.doc.ts +80 -77
- package/ts/db/documents/classes.smartmta-storage.doc.ts +16 -1
- package/ts/db/documents/classes.webpush-admission.doc.ts +14 -5
- package/ts/db/documents/classes.webpush-binding.doc.ts +54 -8
- package/ts/db/documents/classes.webpush-spool.doc.ts +40 -6
- package/ts/dns/manager.dns.ts +6 -2
- package/ts/email/classes.smartmta-storage-manager.ts +66 -50
- package/ts/security/classes.security-policy-manager.ts +10 -8
- package/ts/webpush/classes.webpush-manager.ts +175 -280
- 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
|
|
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
|
-
}):
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
await
|
|
723
|
-
await
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
return rows.map((rowArg) => toPublicWebPushBinding(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
847
|
-
|
|
848
|
-
|
|
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
|
|
860
|
-
if (
|
|
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
|
|
815
|
+
const scrubbed = await WebPushBindingDoc.getInstance({
|
|
873
816
|
id: existing.id,
|
|
874
817
|
lifecycleGeneration,
|
|
875
818
|
deletedAt: existing.deletedAt,
|
|
876
819
|
});
|
|
877
|
-
if (!
|
|
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
|
|
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.
|
|
868
|
+
await WebPushAdmissionDoc.atomicDeleteMany({
|
|
928
869
|
bindingId: scrubbed.id,
|
|
929
870
|
lifecycleGeneration: { $lt: nextLifecycleGeneration },
|
|
930
871
|
});
|
|
931
|
-
const refreshed = await
|
|
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(
|
|
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
|
|
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
|
-
}
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
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 (!
|
|
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
|
|
1120
|
-
|
|
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
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
|
1164
|
-
if (!
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
1257
|
-
if (!
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
|
1336
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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 (!
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
-
}
|
|
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
|
|
1963
|
+
const current = await WebPushAdmissionDoc.getInstance({
|
|
2043
1964
|
bindingId,
|
|
2044
1965
|
lifecycleGeneration,
|
|
2045
1966
|
});
|
|
2046
|
-
if (!
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
2485
|
-
|
|
2486
|
-
.
|
|
2487
|
-
|
|
2488
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
.
|
|
2605
|
-
|
|
2606
|
-
|
|
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
|
|
2609
|
-
const
|
|
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
|
|
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
|
-
|
|
2565
|
+
limit: MAINTENANCE_BATCH_SIZE,
|
|
2566
|
+
});
|
|
2567
|
+
const rows = await cursor.toArray();
|
|
2663
2568
|
let finished = 0;
|
|
2664
|
-
for (const
|
|
2665
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
2758
|
+
const currentSpool = await WebPushSpoolDoc.getInstance({
|
|
2858
2759
|
id: spoolArg.id,
|
|
2859
2760
|
state: 'sending',
|
|
2860
2761
|
leaseToken,
|
|
2861
2762
|
});
|
|
2862
|
-
if (!
|
|
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
|
|
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 (!
|
|
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
|
|
2846
|
+
const immediatelyBeforeSendSpool = await WebPushSpoolDoc.getInstance({
|
|
2948
2847
|
id: currentSpool.id,
|
|
2949
2848
|
state: 'sending',
|
|
2950
2849
|
leaseToken,
|
|
2951
2850
|
});
|
|
2952
|
-
if (!
|
|
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.
|
|
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
|
-
}
|
|
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.
|
|
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
|
|
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 (!
|
|
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
|
|
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: '',
|