Skip to content

Commit 5b0c61a

Browse files
authored
Already deleted Message returns 404 vs 409 conflict when a 2nd attempt to delete is processed (#739)
* deleted message now returns 404 if a second attempt to delete it is attempted, vs a conflict 409 * fix sync edge case for conflicting deletes
1 parent bc54d0c commit 5b0c61a

File tree

4 files changed

+102
-20
lines changed

4 files changed

+102
-20
lines changed

.changeset/dirty-days-flash.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@web5/agent": patch
3+
"@web5/identity-agent": patch
4+
"@web5/proxy-agent": patch
5+
"@web5/user-agent": patch
6+
---
7+
8+
Sync accounts for 404 from a conflicting RecordsDelete message

packages/agent/src/sync-engine-level.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ import type {
55
MessagesQueryReply,
66
MessagesReadReply,
77
PaginationCursor,
8+
UnionMessageReply,
89
} from '@tbd54566975/dwn-sdk-js';
910

1011
import ms from 'ms';
1112
import { Level } from 'level';
1213
import { monotonicFactory } from 'ulidx';
1314
import { NodeStream } from '@web5/common';
15+
import {
16+
DwnInterfaceName,
17+
DwnMethodName,
18+
} from '@tbd54566975/dwn-sdk-js';
1419

1520
import type { SyncEngine } from './types/sync.js';
1621
import type { Web5PlatformAgent } from './types/agent.js';
@@ -143,7 +148,7 @@ export class SyncEngineLevel implements SyncEngine {
143148
: undefined;
144149

145150
const pullReply = await this.agent.dwn.node.processMessage(did, message, { dataStream });
146-
if (pullReply.status.code === 202 || pullReply.status.code === 409) {
151+
if (SyncEngineLevel.syncMessageReplyIsSuccessful(pullReply)) {
147152
await this.addMessage(did, messageCid);
148153
deleteOperations.push({ type: 'del', key: key });
149154
}
@@ -195,7 +200,8 @@ export class SyncEngineLevel implements SyncEngine {
195200
// Update the watermark and add the messageCid to the Sync Message Store if either:
196201
// - 202: message was successfully written to the remote DWN
197202
// - 409: message was already present on the remote DWN
198-
if (reply.status.code === 202 || reply.status.code === 409) {
203+
// - RecordsDelete and the status code is 404: the initial write message was not found or the message was already deleted
204+
if (SyncEngineLevel.syncMessageReplyIsSuccessful(reply)) {
199205
await this.addMessage(did, messageCid);
200206
deleteOperations.push({ type: 'del', key: key });
201207
}
@@ -252,6 +258,17 @@ export class SyncEngineLevel implements SyncEngine {
252258
}
253259
}
254260

261+
private static syncMessageReplyIsSuccessful(reply: UnionMessageReply): boolean {
262+
return reply.status.code === 202 ||
263+
reply.status.code === 409 ||
264+
(
265+
// If the message is a RecordsDelete and the status code is 404, the initial write message was not found or the message was already deleted
266+
reply.entry?.message.descriptor.interface === DwnInterfaceName.Records &&
267+
reply.entry?.message.descriptor.method === DwnMethodName.Delete &&
268+
reply.status.code === 404
269+
);
270+
}
271+
255272
private async enqueueOperations({ syncDirection, syncPeerState }: {
256273
syncDirection: SyncDirection,
257274
syncPeerState: SyncState[]

packages/agent/tests/sync-engine-level.spec.ts

+73-16
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,9 @@ describe('SyncEngineLevel', () => {
435435
it('silently ignores a messageCid that already exists on the local DWN', async () => {
436436
// scenario: The messageCids returned from the remote eventLog contains a messageCid that already exists on the local DWN.
437437
// During sync, when processing the messageCid the local DWN will return a conflict response, but the sync should continue
438+
//
439+
// NOTE: When deleting a message, the conflicting Delete will return a 404 instead of a 409,
440+
// the sync should still mark the message as synced and continue
438441

439442
// create a record and store it locally and remotely
440443
const remoteAndLocalRecord = await testHarness.agent.processDwnRequest({
@@ -456,6 +459,23 @@ describe('SyncEngineLevel', () => {
456459
messageCid : remoteAndLocalRecord.messageCid,
457460
});
458461

462+
// delete the record both locally and remotely
463+
const deleteMessage = await testHarness.agent.processDwnRequest({
464+
author : alice.did.uri,
465+
target : alice.did.uri,
466+
messageType : DwnInterface.RecordsDelete,
467+
messageParams : {
468+
recordId: remoteAndLocalRecord.message!.recordId
469+
}
470+
});
471+
// send the delete to the remote
472+
await testHarness.agent.sendDwnRequest({
473+
author : alice.did.uri,
474+
target : alice.did.uri,
475+
messageType : DwnInterface.RecordsDelete,
476+
messageCid : deleteMessage.messageCid,
477+
});
478+
459479
// create 2 records stored only remotely to later sync to the local DWN
460480
const record1 = await testHarness.agent.sendDwnRequest({
461481
author : alice.did.uri,
@@ -481,7 +501,7 @@ describe('SyncEngineLevel', () => {
481501
});
482502
expect(record2.reply.status.code).to.equal(202);
483503

484-
// confirm that only the single record exists locally
504+
// confirm that only the record and it's delete exists locally
485505
let localQueryResponse = await testHarness.agent.processDwnRequest({
486506
author : alice.did.uri,
487507
target : alice.did.uri,
@@ -492,12 +512,16 @@ describe('SyncEngineLevel', () => {
492512
});
493513

494514
let localDwnQueryEntries = localQueryResponse.reply.entries!;
495-
expect(localDwnQueryEntries.length).to.equal(1);
496-
expect(localDwnQueryEntries).to.have.members([remoteAndLocalRecord.messageCid]);
515+
expect(localDwnQueryEntries.length).to.equal(2);
516+
expect(localDwnQueryEntries).to.have.members([
517+
remoteAndLocalRecord.messageCid,
518+
deleteMessage.messageCid
519+
]);
497520

498521
// stub getDwnEventLog to return the messageCids of the records we want to sync
499522
sinon.stub(syncEngine as any, 'getDwnEventLog').resolves([
500523
remoteAndLocalRecord.messageCid,
524+
deleteMessage.messageCid,
501525
record1.messageCid,
502526
record2.messageCid
503527
]);
@@ -514,17 +538,19 @@ describe('SyncEngineLevel', () => {
514538
// Execute Sync to push records to Alice's remote node
515539
await syncEngine.pull();
516540

517-
// Verify sendDwnRequest is called for all 3 records
518-
expect(sendDwnRequestSpy.callCount).to.equal(3, 'sendDwnRequestSpy');
519-
// Verify that processMessage is called for all 3 records
520-
expect(processMessageSpy.callCount).to.equal(3, 'processMessageSpy');
541+
// Verify sendDwnRequest is called for all 4 messages
542+
expect(sendDwnRequestSpy.callCount).to.equal(4, 'sendDwnRequestSpy');
543+
// Verify that processMessage is called for all 4 messages
544+
expect(processMessageSpy.callCount).to.equal(4, 'processMessageSpy');
521545

522546
// Verify that the conflict response is returned for the record that already exists locally
523547
expect((await processMessageSpy.firstCall.returnValue).status.code).to.equal(409);
548+
// Verify that the delete message returned a 404
549+
expect((await processMessageSpy.secondCall.returnValue).status.code).to.equal(404);
524550

525551
// Verify that the other 2 records are successfully processed
526-
expect((await processMessageSpy.secondCall.returnValue).status.code).to.equal(202);
527-
expect((await processMessageSpy.thirdCall.returnValue).status.code).to.equal(202);
552+
expect((await processMessageSpy.returnValues[2]).status.code).to.equal(202);
553+
expect((await processMessageSpy.returnValues[3]).status.code).to.equal(202);
528554

529555
// confirm the new records exist remotely
530556
localQueryResponse = await testHarness.agent.processDwnRequest({
@@ -536,9 +562,10 @@ describe('SyncEngineLevel', () => {
536562
},
537563
});
538564
localDwnQueryEntries = localQueryResponse.reply.entries!;
539-
expect(localDwnQueryEntries.length).to.equal(3);
565+
expect(localDwnQueryEntries.length).to.equal(4);
540566
expect(localDwnQueryEntries).to.have.members([
541567
remoteAndLocalRecord.messageCid,
568+
deleteMessage.messageCid,
542569
record1.messageCid,
543570
record2.messageCid
544571
]);
@@ -901,6 +928,8 @@ describe('SyncEngineLevel', () => {
901928

902929
// scenario: The messageCids returned from the local eventLog contains a Cid that already exists in the remote DWN.
903930
// During sync, the remote DWN will return a conflict 409 status code and the sync should continue
931+
// NOTE: if the messageCid is a delete message and it is already deleted,
932+
// the remote DWN will return a 404 status code and the sync should continue
904933

905934
// create a record, store it and send it to the remote Dwn
906935
const remoteAndLocalRecord = await testHarness.agent.processDwnRequest({
@@ -922,6 +951,23 @@ describe('SyncEngineLevel', () => {
922951
messageCid : remoteAndLocalRecord.messageCid,
923952
});
924953

954+
// delete the record both locally and remotely
955+
const deleteMessage = await testHarness.agent.processDwnRequest({
956+
author : alice.did.uri,
957+
target : alice.did.uri,
958+
messageType : DwnInterface.RecordsDelete,
959+
messageParams : {
960+
recordId: remoteAndLocalRecord.message!.recordId
961+
}
962+
});
963+
// send the delete to the remote
964+
await testHarness.agent.sendDwnRequest({
965+
author : alice.did.uri,
966+
target : alice.did.uri,
967+
messageType : DwnInterface.RecordsDelete,
968+
messageCid : deleteMessage.messageCid,
969+
});
970+
925971
// create 2 records stored only locally to sync to the remote DWN
926972
const record1 = await testHarness.agent.processDwnRequest({
927973
author : alice.did.uri,
@@ -947,7 +993,7 @@ describe('SyncEngineLevel', () => {
947993
});
948994
expect(record2.reply.status.code).to.equal(202);
949995

950-
// confirm that only the single record exists remotely
996+
// confirm that only record and it's delete exist remotely
951997
let remoteQueryResponse = await testHarness.agent.sendDwnRequest({
952998
author : alice.did.uri,
953999
target : alice.did.uri,
@@ -958,13 +1004,14 @@ describe('SyncEngineLevel', () => {
9581004
});
9591005

9601006
let remoteDwnQueryEntries = remoteQueryResponse.reply.entries!;
961-
expect(remoteDwnQueryEntries.length).to.equal(1);
962-
expect(remoteDwnQueryEntries).to.have.members([remoteAndLocalRecord.messageCid]);
1007+
expect(remoteDwnQueryEntries.length).to.equal(2);
1008+
expect(remoteDwnQueryEntries).to.have.members([ remoteAndLocalRecord.messageCid, deleteMessage.messageCid ]);
9631009

9641010
// stub getDwnEventLog to return the messageCids of the records we want to sync
9651011
// we stub this to avoid syncing the registered identity related messages
9661012
sinon.stub(syncEngine as any, 'getDwnEventLog').resolves([
9671013
remoteAndLocalRecord.messageCid,
1014+
deleteMessage.messageCid,
9681015
record1.messageCid,
9691016
record2.messageCid
9701017
]);
@@ -975,8 +1022,17 @@ describe('SyncEngineLevel', () => {
9751022
// Execute Sync to push records to Alice's remote node
9761023
await syncEngine.push();
9771024

978-
// Verify sendDwnRequest was called once for each record including the one that already exists remotely
979-
expect(sendDwnRequestSpy.callCount).to.equal(3);
1025+
// Verify sendDwnRequest was called once for each record including the ones that already exist remotely
1026+
expect(sendDwnRequestSpy.callCount).to.equal(4);
1027+
1028+
// Verify that the conflict response is returned for the record that already exists remotely
1029+
expect((await sendDwnRequestSpy.firstCall.returnValue).status.code).to.equal(409);
1030+
// Verify that the delete message returned a 404
1031+
expect((await sendDwnRequestSpy.secondCall.returnValue).status.code).to.equal(404);
1032+
1033+
// Verify that the other 2 records are successfully processed
1034+
expect((await sendDwnRequestSpy.returnValues[2]).status.code).to.equal(202);
1035+
expect((await sendDwnRequestSpy.returnValues[3]).status.code).to.equal(202);
9801036

9811037
// confirm the new records exist remotely
9821038
remoteQueryResponse = await testHarness.agent.sendDwnRequest({
@@ -988,9 +1044,10 @@ describe('SyncEngineLevel', () => {
9881044
},
9891045
});
9901046
remoteDwnQueryEntries = remoteQueryResponse.reply.entries!;
991-
expect(remoteDwnQueryEntries.length).to.equal(3);
1047+
expect(remoteDwnQueryEntries.length).to.equal(4);
9921048
expect(remoteDwnQueryEntries).to.have.members([
9931049
remoteAndLocalRecord.messageCid,
1050+
deleteMessage.messageCid,
9941051
record1.messageCid,
9951052
record2.messageCid
9961053
]);

packages/api/tests/record.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2983,7 +2983,7 @@ describe('Record', () => {
29832983
}
29842984
});
29852985

2986-
it('duplicate delete with store should return conflict', async () => {
2986+
it('duplicate delete with store should return not found', async () => {
29872987
// create a record
29882988
const { status: writeStatus, record } = await dwnAlice.records.write({
29892989
data : 'Hello, world!',
@@ -3013,7 +3013,7 @@ describe('Record', () => {
30133013

30143014
// attempt to delete the record again
30153015
const { status: deleteStatus2 } = await record.delete();
3016-
expect(deleteStatus2.code).to.equal(409);
3016+
expect(deleteStatus2.code).to.equal(404);
30173017
});
30183018

30193019
it('a record in a deleted state returns undefined for data related fields', async () => {

0 commit comments

Comments
 (0)