Skip to content

Commit fb09f02

Browse files
author
code4bones
committed
Settle successfully sent PWA read receipts
1 parent d146570 commit fb09f02

6 files changed

Lines changed: 23 additions & 23 deletions

File tree

‎pwa/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pwa/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@code4bones/branch-pwa",
3-
"version": "0.1.0-beta.151",
3+
"version": "0.1.0-beta.152",
44
"private": true,
55
"type": "module",
66
"scripts": {

‎pwa/src/connectivity/delivery-receipt-control.ts‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ export const deliveryReceiptControlDescriptor: ApplicationControlDescriptor<Deli
5959
const receiptRegistry = createApplicationControlRegistry([deliveryReceiptControlDescriptor]);
6060

6161
/**
62-
* Sends one signed receipt. The default target dedup protects the live
63-
* delivered-receipt path from duplicate inbound envelopes. A durable local
64-
* read-presentation record may explicitly make a bounded re-attempt; that
65-
* creates a fresh, signed and independently replay-protected control.
62+
* Sends one signed receipt. Target dedup protects the live delivered- and
63+
* read-receipt paths from duplicate inbound envelopes. A failed local send
64+
* releases its dedup key, so the durable read outbox can make a later bounded
65+
* retry without emitting a second control after successful hand-off.
6666
*/
6767
export async function sendDeliveryReceipt(options: {
6868
readonly kind: DeliveryReceiptKind;
@@ -71,15 +71,14 @@ export async function sendDeliveryReceipt(options: {
7171
readonly recipientPeerId: string;
7272
readonly recipientHpkePublicKey: string;
7373
readonly attached: boolean;
74-
readonly allowTargetRetry?: boolean;
7574
}): Promise<DeliveryReceiptSendResult> {
7675
if (!options.attached || !validDeliveryId(options.targetDeliveryId)) return "skipped";
7776
const keys = getLocalIdentityKeys();
7877
if (keys === null) return "skipped";
7978
const now = Date.now();
8079
prune(now);
8180
const emittedKey = `${options.kind}\n${options.recipientPeerId}\n${options.targetDeliveryId}`;
82-
if (!options.allowTargetRetry && emittedReceiptTargets.has(emittedKey)) return "skipped";
81+
if (emittedReceiptTargets.has(emittedKey)) return "skipped";
8382
let unsigned: ReturnType<typeof prepareOutboundApplicationControl<DeliveryReceiptBody>>;
8483
try {
8584
unsigned = prepareOutboundApplicationControl({
@@ -96,7 +95,7 @@ export async function sendDeliveryReceipt(options: {
9695
isKnownContact: (peerId) => peerId === options.recipientPeerId,
9796
isAllowed: () => true,
9897
consumeRateLimit: () => {
99-
if (!options.allowTargetRetry && emittedReceiptTargets.has(emittedKey)) return false;
98+
if (emittedReceiptTargets.has(emittedKey)) return false;
10099
if (emittedReceiptTargets.size >= maxReplayEntries) {
101100
const oldest = emittedReceiptTargets.keys().next().value;
102101
if (oldest !== undefined) emittedReceiptTargets.delete(oldest);

‎pwa/src/connectivity/read-receipt-runtime.ts‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,16 +116,16 @@ async function run(runtime: ReadReceiptRuntime): Promise<void> {
116116
recipientPeerId: contact.peerId,
117117
recipientHpkePublicKey: contact.hpkePublicKey,
118118
attached: true,
119-
allowTargetRetry: true
120119
});
121120
if (result === "sent") {
122-
// Local admission to the attachment does not prove the recipient
123-
// received the control. Retain this device-local presentation fact
124-
// for a small bounded retry window; a later signed read remains
125-
// the sender's sole proof for rendering Read.
126-
const next = nextRetryAt(Date.now(), expiresAt);
127-
runtime.retryAtByTarget.set(entry.targetDeliveryId, next);
128-
nextAttemptAt = earliest(nextAttemptAt, next);
121+
// A successful hand-off has consumed this local work item. Retrying
122+
// it with a fresh signed control makes a healthy recipient see an
123+
// otherwise-valid duplicate after it has already consumed the
124+
// delivery mapping, which is indistinguishable from an unmatched
125+
// receipt. Failures below remain bounded retries because the send
126+
// routine releases its local dedup key before it throws.
127+
runtime.retryAtByTarget.delete(entry.targetDeliveryId);
128+
state.settleReadReceipt(entry.targetDeliveryId);
129129
state.recordTransportTrace("delivery receipt: read_attempt_sent");
130130
continue;
131131
}

‎pwa/tests/app-shell.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,8 @@ void test("PWA persists active-chat read presentation locally and drains opted-i
843843
assert.match(runtime, /attachment-driven, never presence-driven/);
844844
assert.match(runtime, /deliveryReceiptControlTTLms/);
845845
assert.match(runtime, /readReceiptRetryDelayMs = 30_000/);
846-
assert.match(runtime, /allowTargetRetry: true/);
846+
assert.match(runtime, /state\.settleReadReceipt\(entry\.targetDeliveryId\)/);
847+
assert.doesNotMatch(runtime, /allowTargetRetry/);
847848
assert.match(runtime, /read_attempt_sent/);
848849
assert.match(runtime, /read_expired/);
849850
assert.match(runtime, /read_failed_\$\{readReceiptFailure/);

‎pwa/tests/delivery-receipt-control.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ void test("receipt body has a closed deterministic shape", () => {
9595
assert.throws(() => encodeReceiptBody({ kind: "read", targetDeliveryId: "wrong" }));
9696
});
9797

98-
void test("read receipt sender keeps default duplicate suppression but exposes only an explicit retry escape hatch", async () => {
98+
void test("read receipt sender deduplicates a successfully handed-off target", async () => {
9999
const source = await readFile(resolve(process.cwd(), "src/connectivity/delivery-receipt-control.ts"), "utf8");
100-
assert.match(source, /readonly allowTargetRetry\?: boolean/);
101-
assert.match(source, /!options\.allowTargetRetry && emittedReceiptTargets\.has\(emittedKey\)/);
102-
assert.match(source, /!options\.allowTargetRetry && emittedReceiptTargets\.has\(emittedKey\)\) return false/);
100+
assert.match(source, /if \(emittedReceiptTargets\.has\(emittedKey\)\) return "skipped"/);
101+
assert.match(source, /if \(emittedReceiptTargets\.has\(emittedKey\)\) return false/);
102+
assert.doesNotMatch(source, /allowTargetRetry/);
103103
});
104104

105105
async function signedReceipt(

0 commit comments

Comments
 (0)