@enbox/agent 0.1.5 → 0.1.7
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/dist/browser.mjs +11 -11
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/anonymous-dwn-api.js +184 -0
- package/dist/esm/anonymous-dwn-api.js.map +1 -0
- package/dist/esm/dwn-api.js +85 -785
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-encryption.js +342 -0
- package/dist/esm/dwn-encryption.js.map +1 -0
- package/dist/esm/dwn-key-delivery.js +256 -0
- package/dist/esm/dwn-key-delivery.js.map +1 -0
- package/dist/esm/dwn-record-upgrade.js +119 -0
- package/dist/esm/dwn-record-upgrade.js.map +1 -0
- package/dist/esm/dwn-type-guards.js +23 -0
- package/dist/esm/dwn-type-guards.js.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/permissions-api.js +43 -2
- package/dist/esm/permissions-api.js.map +1 -1
- package/dist/esm/protocol-utils.js +158 -0
- package/dist/esm/protocol-utils.js.map +1 -0
- package/dist/esm/store-data-protocols.js +1 -1
- package/dist/esm/store-data-protocols.js.map +1 -1
- package/dist/esm/store-data.js +3 -0
- package/dist/esm/store-data.js.map +1 -1
- package/dist/esm/sync-engine-level.js +23 -354
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-messages.js +237 -0
- package/dist/esm/sync-messages.js.map +1 -0
- package/dist/esm/sync-topological-sort.js +143 -0
- package/dist/esm/sync-topological-sort.js.map +1 -0
- package/dist/esm/test-harness.js +20 -0
- package/dist/esm/test-harness.js.map +1 -1
- package/dist/types/anonymous-dwn-api.d.ts +140 -0
- package/dist/types/anonymous-dwn-api.d.ts.map +1 -0
- package/dist/types/dwn-api.d.ts +36 -184
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-encryption.d.ts +144 -0
- package/dist/types/dwn-encryption.d.ts.map +1 -0
- package/dist/types/dwn-key-delivery.d.ts +112 -0
- package/dist/types/dwn-key-delivery.d.ts.map +1 -0
- package/dist/types/dwn-record-upgrade.d.ts +33 -0
- package/dist/types/dwn-record-upgrade.d.ts.map +1 -0
- package/dist/types/dwn-type-guards.d.ts +9 -0
- package/dist/types/dwn-type-guards.d.ts.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/permissions-api.d.ts +6 -1
- package/dist/types/permissions-api.d.ts.map +1 -1
- package/dist/types/protocol-utils.d.ts +70 -0
- package/dist/types/protocol-utils.d.ts.map +1 -0
- package/dist/types/store-data.d.ts +4 -0
- package/dist/types/store-data.d.ts.map +1 -1
- package/dist/types/sync-engine-level.d.ts +5 -42
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/dist/types/sync-messages.d.ts +76 -0
- package/dist/types/sync-messages.d.ts.map +1 -0
- package/dist/types/sync-topological-sort.d.ts +15 -0
- package/dist/types/sync-topological-sort.d.ts.map +1 -0
- package/dist/types/test-harness.d.ts +10 -0
- package/dist/types/test-harness.d.ts.map +1 -1
- package/dist/types/types/permissions.d.ts +2 -0
- package/dist/types/types/permissions.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/anonymous-dwn-api.ts +263 -0
- package/src/dwn-api.ts +158 -1024
- package/src/dwn-encryption.ts +481 -0
- package/src/dwn-key-delivery.ts +370 -0
- package/src/dwn-record-upgrade.ts +166 -0
- package/src/dwn-type-guards.ts +43 -0
- package/src/index.ts +6 -0
- package/src/permissions-api.ts +54 -2
- package/src/protocol-utils.ts +185 -0
- package/src/store-data-protocols.ts +1 -1
- package/src/store-data.ts +5 -2
- package/src/sync-engine-level.ts +25 -414
- package/src/sync-messages.ts +279 -0
- package/src/sync-topological-sort.ts +167 -0
- package/src/test-harness.ts +19 -0
- package/src/types/permissions.ts +2 -0
package/src/sync-engine-level.ts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
import type { AbstractLevel } from 'abstract-level';
|
|
2
|
-
import type {
|
|
3
|
-
GenericMessage,
|
|
4
|
-
MessagesReadReply,
|
|
5
|
-
MessagesSyncReply,
|
|
6
|
-
UnionMessageReply,
|
|
7
|
-
} from '@enbox/dwn-sdk-js';
|
|
2
|
+
import type { GenericMessage, MessagesSyncReply } from '@enbox/dwn-sdk-js';
|
|
8
3
|
|
|
9
4
|
import ms from 'ms';
|
|
10
5
|
|
|
11
6
|
import { Level } from 'level';
|
|
12
|
-
import {
|
|
13
|
-
DwnInterfaceName,
|
|
14
|
-
DwnMethodName,
|
|
15
|
-
Message,
|
|
16
|
-
PermissionsProtocol,
|
|
17
|
-
} from '@enbox/dwn-sdk-js';
|
|
18
7
|
import { hashToHex, initDefaultHashes } from '@enbox/dwn-sdk-js';
|
|
19
8
|
|
|
20
9
|
import type { PermissionsApi } from './types/permissions.js';
|
|
@@ -23,7 +12,9 @@ import type { Web5Agent, Web5PlatformAgent } from './types/agent.js';
|
|
|
23
12
|
|
|
24
13
|
import { AgentPermissionsApi } from './permissions-api.js';
|
|
25
14
|
import { DwnInterface } from './types/dwn.js';
|
|
26
|
-
import { getDwnServiceEndpointUrls
|
|
15
|
+
import { getDwnServiceEndpointUrls } from './utils.js';
|
|
16
|
+
import { topologicalSort } from './sync-topological-sort.js';
|
|
17
|
+
import { pullMessages, pushMessages } from './sync-messages.js';
|
|
27
18
|
|
|
28
19
|
export type SyncEngineLevelParams = {
|
|
29
20
|
agent?: Web5PlatformAgent;
|
|
@@ -196,7 +187,7 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
196
187
|
}
|
|
197
188
|
}
|
|
198
189
|
} catch (error: any) {
|
|
199
|
-
//
|
|
190
|
+
// Skip this DWN endpoint for remaining targets and log the real cause.
|
|
200
191
|
errored.add(dwnUrl);
|
|
201
192
|
console.error(`SyncEngineLevel: Error syncing ${did} with ${dwnUrl}`, error);
|
|
202
193
|
}
|
|
@@ -520,14 +511,13 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
520
511
|
// Pull — fetch messages from remote, process locally in dependency order
|
|
521
512
|
// ---------------------------------------------------------------------------
|
|
522
513
|
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
// Pull / Push — delegates to standalone functions in sync-messages.ts
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
|
|
523
518
|
/**
|
|
524
|
-
* Fetches missing messages from the remote DWN and processes them
|
|
519
|
+
* Fetches missing messages from the remote DWN and processes them locally
|
|
525
520
|
* in dependency order (topological sort).
|
|
526
|
-
*
|
|
527
|
-
* Messages that fail processing are re-fetched from the remote before each retry
|
|
528
|
-
* pass rather than buffered in memory. ReadableStream is single-use, so a failed
|
|
529
|
-
* message's data stream is consumed on the first attempt. Re-fetching provides a
|
|
530
|
-
* fresh stream without holding all record data in memory simultaneously.
|
|
531
521
|
*/
|
|
532
522
|
private async pullMessages({ did, dwnUrl, delegateDid, protocol, messageCids }: {
|
|
533
523
|
did: string;
|
|
@@ -536,132 +526,16 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
536
526
|
protocol?: string;
|
|
537
527
|
messageCids: string[];
|
|
538
528
|
}): Promise<void> {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
// Step 3: Process messages in dependency order with multi-pass retry.
|
|
546
|
-
// Retry up to MAX_RETRY_PASSES times for messages that fail due to
|
|
547
|
-
// dependency ordering issues (e.g., a RecordsWrite whose ProtocolsConfigure
|
|
548
|
-
// hasn't committed yet). Failed messages are re-fetched from the remote
|
|
549
|
-
// to obtain a fresh data stream, since ReadableStream is single-use.
|
|
550
|
-
const MAX_RETRY_PASSES = 3;
|
|
551
|
-
let pending = sorted;
|
|
552
|
-
|
|
553
|
-
for (let pass = 0; pass <= MAX_RETRY_PASSES && pending.length > 0; pass++) {
|
|
554
|
-
const failedCids: string[] = [];
|
|
555
|
-
|
|
556
|
-
for (const entry of pending) {
|
|
557
|
-
const pullReply = await this.agent.dwn.node.processMessage(did, entry.message, { dataStream: entry.dataStream });
|
|
558
|
-
if (!SyncEngineLevel.syncMessageReplyIsSuccessful(pullReply)) {
|
|
559
|
-
const cid = await SyncEngineLevel.getMessageCid(entry.message);
|
|
560
|
-
failedCids.push(cid);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Re-fetch failed messages from the remote to get fresh data streams.
|
|
565
|
-
if (failedCids.length > 0) {
|
|
566
|
-
const reFetched = await this.fetchRemoteMessages({ did, dwnUrl, delegateDid, protocol, messageCids: failedCids });
|
|
567
|
-
pending = SyncEngineLevel.topologicalSort(reFetched);
|
|
568
|
-
} else {
|
|
569
|
-
pending = [];
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Fetches messages from a remote DWN by their CIDs using MessagesRead.
|
|
576
|
-
*/
|
|
577
|
-
private async fetchRemoteMessages({ did, dwnUrl, delegateDid, protocol, messageCids }: {
|
|
578
|
-
did: string;
|
|
579
|
-
dwnUrl: string;
|
|
580
|
-
delegateDid?: string;
|
|
581
|
-
protocol?: string;
|
|
582
|
-
messageCids: string[];
|
|
583
|
-
}): Promise<{ message: GenericMessage; dataStream?: ReadableStream<Uint8Array> }[]> {
|
|
584
|
-
const results: { message: GenericMessage; dataStream?: ReadableStream<Uint8Array> }[] = [];
|
|
585
|
-
|
|
586
|
-
let permissionGrantId: string | undefined;
|
|
587
|
-
if (delegateDid) {
|
|
588
|
-
try {
|
|
589
|
-
const messagesReadGrant = await this._permissionsApi.getPermissionForRequest({
|
|
590
|
-
connectedDid : did,
|
|
591
|
-
messageType : DwnInterface.MessagesRead,
|
|
592
|
-
delegateDid,
|
|
593
|
-
protocol,
|
|
594
|
-
cached : true
|
|
595
|
-
});
|
|
596
|
-
permissionGrantId = messagesReadGrant.grant.id;
|
|
597
|
-
} catch (error: any) {
|
|
598
|
-
console.error('SyncEngineLevel: pull - Error fetching MessagesRead permission grant for delegate DID', error);
|
|
599
|
-
return results;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Fetch messages in parallel with bounded concurrency.
|
|
604
|
-
const CONCURRENCY = 10;
|
|
605
|
-
let cursor = 0;
|
|
606
|
-
|
|
607
|
-
while (cursor < messageCids.length) {
|
|
608
|
-
const batch = messageCids.slice(cursor, cursor + CONCURRENCY);
|
|
609
|
-
cursor += CONCURRENCY;
|
|
610
|
-
|
|
611
|
-
type FetchResult = { message: GenericMessage; dataStream?: ReadableStream<Uint8Array> } | undefined;
|
|
612
|
-
const batchResults = await Promise.all(batch.map(async (messageCid): Promise<FetchResult> => {
|
|
613
|
-
const messagesRead = await this.agent.processDwnRequest({
|
|
614
|
-
store : false,
|
|
615
|
-
author : did,
|
|
616
|
-
target : did,
|
|
617
|
-
messageType : DwnInterface.MessagesRead,
|
|
618
|
-
granteeDid : delegateDid,
|
|
619
|
-
messageParams : { messageCid, permissionGrantId }
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
let reply: MessagesReadReply;
|
|
623
|
-
try {
|
|
624
|
-
reply = await this.agent.rpc.sendDwnRequest({
|
|
625
|
-
dwnUrl,
|
|
626
|
-
targetDid : did,
|
|
627
|
-
message : messagesRead.message,
|
|
628
|
-
}) as MessagesReadReply;
|
|
629
|
-
} catch {
|
|
630
|
-
return undefined;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
if (reply.status.code !== 200 || !reply.entry?.message) {
|
|
634
|
-
return undefined;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
const replyEntry = reply.entry;
|
|
638
|
-
let dataStream: ReadableStream<Uint8Array> | undefined;
|
|
639
|
-
if (isRecordsWrite(replyEntry) && replyEntry.data) {
|
|
640
|
-
dataStream = replyEntry.data;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
return { message: replyEntry.message, dataStream };
|
|
644
|
-
}));
|
|
645
|
-
|
|
646
|
-
for (const result of batchResults) {
|
|
647
|
-
if (result) {
|
|
648
|
-
results.push(result);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
return results;
|
|
529
|
+
return pullMessages({
|
|
530
|
+
did, dwnUrl, delegateDid, protocol, messageCids,
|
|
531
|
+
agent : this.agent,
|
|
532
|
+
permissionsApi : this._permissionsApi,
|
|
533
|
+
});
|
|
654
534
|
}
|
|
655
535
|
|
|
656
|
-
// ---------------------------------------------------------------------------
|
|
657
|
-
// Push — read local messages, send to remote
|
|
658
|
-
// ---------------------------------------------------------------------------
|
|
659
|
-
|
|
660
536
|
/**
|
|
661
|
-
* Reads missing messages from the local DWN and pushes them to the remote DWN
|
|
662
|
-
*
|
|
663
|
-
* so that initial writes come before updates, and ProtocolsConfigures come before
|
|
664
|
-
* records that reference those protocols.
|
|
537
|
+
* Reads missing messages from the local DWN and pushes them to the remote DWN
|
|
538
|
+
* in dependency order (topological sort).
|
|
665
539
|
*/
|
|
666
540
|
private async pushMessages({ did, dwnUrl, delegateDid, protocol, messageCids }: {
|
|
667
541
|
did: string;
|
|
@@ -670,288 +544,25 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
670
544
|
protocol?: string;
|
|
671
545
|
messageCids: string[];
|
|
672
546
|
}): Promise<void> {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (dwnMessage) {
|
|
678
|
-
fetched.push(dwnMessage);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// Step 2: Sort in dependency order using topological sort.
|
|
683
|
-
const sorted = SyncEngineLevel.topologicalSort(fetched);
|
|
684
|
-
|
|
685
|
-
// Step 3: Push messages in dependency order, consuming each stream as we go.
|
|
686
|
-
for (const entry of sorted) {
|
|
687
|
-
try {
|
|
688
|
-
const reply = await this.agent.rpc.sendDwnRequest({
|
|
689
|
-
dwnUrl,
|
|
690
|
-
targetDid : did,
|
|
691
|
-
data : entry.dataStream,
|
|
692
|
-
message : entry.message
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
if (!SyncEngineLevel.syncMessageReplyIsSuccessful(reply)) {
|
|
696
|
-
const cid = await SyncEngineLevel.getMessageCid(entry.message);
|
|
697
|
-
console.error(`SyncEngineLevel: push failed for ${cid}: ${reply.status.code} ${reply.status.detail}`);
|
|
698
|
-
}
|
|
699
|
-
} catch {
|
|
700
|
-
// Remote unreachable — stop pushing to this endpoint.
|
|
701
|
-
throw new Error(`SyncEngineLevel: Remote DWN at ${dwnUrl} is unreachable.`);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* Helper to get the CID of a message for logging purposes.
|
|
708
|
-
*/
|
|
709
|
-
private static async getMessageCid(message: GenericMessage): Promise<string> {
|
|
710
|
-
try {
|
|
711
|
-
return await Message.getCid(message);
|
|
712
|
-
} catch {
|
|
713
|
-
return 'unknown';
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Reads a message from the local DWN by its CID using MessagesRead.
|
|
719
|
-
*/
|
|
720
|
-
private async getLocalMessage({ author, delegateDid, protocol, messageCid }: {
|
|
721
|
-
author: string;
|
|
722
|
-
delegateDid?: string;
|
|
723
|
-
protocol?: string;
|
|
724
|
-
messageCid: string;
|
|
725
|
-
}): Promise<{ message: GenericMessage; dataStream?: ReadableStream<Uint8Array> } | undefined> {
|
|
726
|
-
let permissionGrantId: string | undefined;
|
|
727
|
-
if (delegateDid) {
|
|
728
|
-
try {
|
|
729
|
-
const messagesReadGrant = await this._permissionsApi.getPermissionForRequest({
|
|
730
|
-
connectedDid : author,
|
|
731
|
-
messageType : DwnInterface.MessagesRead,
|
|
732
|
-
delegateDid,
|
|
733
|
-
protocol,
|
|
734
|
-
cached : true
|
|
735
|
-
});
|
|
736
|
-
permissionGrantId = messagesReadGrant.grant.id;
|
|
737
|
-
} catch (error: any) {
|
|
738
|
-
console.error('SyncEngineLevel: push - Error fetching MessagesRead permission grant for delegate DID', error);
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
const { reply } = await this.agent.dwn.processRequest({
|
|
744
|
-
author,
|
|
745
|
-
target : author,
|
|
746
|
-
messageType : DwnInterface.MessagesRead,
|
|
747
|
-
granteeDid : delegateDid,
|
|
748
|
-
messageParams : { messageCid, permissionGrantId }
|
|
547
|
+
return pushMessages({
|
|
548
|
+
did, dwnUrl, delegateDid, protocol, messageCids,
|
|
549
|
+
agent : this.agent,
|
|
550
|
+
permissionsApi : this._permissionsApi,
|
|
749
551
|
});
|
|
750
|
-
|
|
751
|
-
if (reply.status.code !== 200 || !reply.entry) {
|
|
752
|
-
return undefined;
|
|
753
|
-
}
|
|
754
|
-
const messageEntry = reply.entry!;
|
|
755
|
-
|
|
756
|
-
const result: { message: GenericMessage; dataStream?: ReadableStream<Uint8Array> } = {
|
|
757
|
-
message: messageEntry.message
|
|
758
|
-
};
|
|
759
|
-
|
|
760
|
-
if (isRecordsWrite(messageEntry) && messageEntry.data) {
|
|
761
|
-
result.dataStream = messageEntry.data;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
return result;
|
|
765
552
|
}
|
|
766
553
|
|
|
767
554
|
// ---------------------------------------------------------------------------
|
|
768
|
-
// Dependency-aware topological sort
|
|
555
|
+
// Dependency-aware topological sort — delegates to sync-topological-sort.ts
|
|
769
556
|
// ---------------------------------------------------------------------------
|
|
770
557
|
|
|
771
558
|
/**
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
* Dependencies:
|
|
776
|
-
* - ProtocolsConfigure must come before any RecordsWrite using that protocol
|
|
777
|
-
* - Parent record must come before child record (via parentId)
|
|
778
|
-
* - Initial write must come before update writes (same recordId, not initial)
|
|
779
|
-
* - Permission grant must come before records using that permissionGrantId
|
|
559
|
+
* Delegate to the standalone `topologicalSort` function.
|
|
560
|
+
* Tests call `SyncEngineLevel.topologicalSort(...)` so this static method must remain.
|
|
780
561
|
*/
|
|
781
562
|
static topologicalSort<T extends { message: GenericMessage }>(
|
|
782
563
|
messages: T[]
|
|
783
564
|
): T[] {
|
|
784
|
-
|
|
785
|
-
return messages;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// Index messages by various keys for dependency resolution.
|
|
789
|
-
const byIndex = new Map<number, T>();
|
|
790
|
-
const protocolConfigureIndex = new Map<string, number>(); // protocol URL -> index
|
|
791
|
-
const initialWriteIndex = new Map<string, number>(); // recordId -> index of initial write
|
|
792
|
-
const grantIndex = new Map<string, number>(); // grant recordId -> index
|
|
793
|
-
|
|
794
|
-
for (let i = 0; i < messages.length; i++) {
|
|
795
|
-
const entry = messages[i];
|
|
796
|
-
byIndex.set(i, entry);
|
|
797
|
-
const desc = entry.message.descriptor;
|
|
798
|
-
|
|
799
|
-
if (desc.interface === DwnInterfaceName.Protocols && desc.method === DwnMethodName.Configure) {
|
|
800
|
-
const protocolUrl = (desc as any).definition?.protocol;
|
|
801
|
-
if (protocolUrl) {
|
|
802
|
-
protocolConfigureIndex.set(protocolUrl, i);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
if (desc.interface === DwnInterfaceName.Records && desc.method === DwnMethodName.Write) {
|
|
807
|
-
const recordId = (entry.message as any).recordId;
|
|
808
|
-
const isInitial = SyncEngineLevel.isInitialWrite(entry.message);
|
|
809
|
-
if (isInitial && recordId) {
|
|
810
|
-
initialWriteIndex.set(recordId, i);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// Index permission grants by recordId so dependents can reference them.
|
|
814
|
-
if (
|
|
815
|
-
(desc as any).protocol === PermissionsProtocol.uri &&
|
|
816
|
-
(desc as any).protocolPath === PermissionsProtocol.grantPath &&
|
|
817
|
-
recordId
|
|
818
|
-
) {
|
|
819
|
-
grantIndex.set(recordId, i);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// Build adjacency list (edges: dependency -> dependent).
|
|
825
|
-
const edges = new Map<number, Set<number>>();
|
|
826
|
-
const inDegree = new Array(messages.length).fill(0) as number[];
|
|
827
|
-
|
|
828
|
-
const addEdge = (from: number, to: number): void => {
|
|
829
|
-
if (from === to) {
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
if (!edges.has(from)) {
|
|
833
|
-
edges.set(from, new Set());
|
|
834
|
-
}
|
|
835
|
-
const edgeSet = edges.get(from)!;
|
|
836
|
-
if (!edgeSet.has(to)) {
|
|
837
|
-
edgeSet.add(to);
|
|
838
|
-
inDegree[to]++;
|
|
839
|
-
}
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
for (let i = 0; i < messages.length; i++) {
|
|
843
|
-
const desc = messages[i].message.descriptor;
|
|
844
|
-
const msg = messages[i].message as any;
|
|
845
|
-
|
|
846
|
-
// Protocol dependency: RecordsWrite depends on ProtocolsConfigure for its protocol.
|
|
847
|
-
if (desc.interface === DwnInterfaceName.Records) {
|
|
848
|
-
const protocol = (desc as any).protocol;
|
|
849
|
-
if (protocol && protocolConfigureIndex.has(protocol)) {
|
|
850
|
-
addEdge(protocolConfigureIndex.get(protocol)!, i);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// Parent dependency: child record depends on parent record.
|
|
855
|
-
if (desc.interface === DwnInterfaceName.Records && (desc as any).parentId) {
|
|
856
|
-
const parentId = (desc as any).parentId;
|
|
857
|
-
if (initialWriteIndex.has(parentId)) {
|
|
858
|
-
addEdge(initialWriteIndex.get(parentId)!, i);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// Initial write dependency: update depends on initial write.
|
|
863
|
-
if (desc.interface === DwnInterfaceName.Records && desc.method === DwnMethodName.Write) {
|
|
864
|
-
const recordId = msg.recordId;
|
|
865
|
-
if (recordId && !SyncEngineLevel.isInitialWrite(messages[i].message) && initialWriteIndex.has(recordId)) {
|
|
866
|
-
addEdge(initialWriteIndex.get(recordId)!, i);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// Delete depends on initial write.
|
|
871
|
-
if (desc.interface === DwnInterfaceName.Records && desc.method === DwnMethodName.Delete) {
|
|
872
|
-
const recordId = msg.descriptor?.recordId;
|
|
873
|
-
if (recordId && initialWriteIndex.has(recordId)) {
|
|
874
|
-
addEdge(initialWriteIndex.get(recordId)!, i);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// Permission grant dependency: message depends on the grant it references.
|
|
879
|
-
const permissionGrantId = (desc as any).permissionGrantId;
|
|
880
|
-
if (permissionGrantId && grantIndex.has(permissionGrantId)) {
|
|
881
|
-
addEdge(grantIndex.get(permissionGrantId)!, i);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
// Kahn's algorithm for topological sort.
|
|
886
|
-
const queue: number[] = [];
|
|
887
|
-
for (let i = 0; i < messages.length; i++) {
|
|
888
|
-
if (inDegree[i] === 0) {
|
|
889
|
-
queue.push(i);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const sorted: T[] = [];
|
|
894
|
-
while (queue.length > 0) {
|
|
895
|
-
const node = queue.shift()!;
|
|
896
|
-
sorted.push(byIndex.get(node)!);
|
|
897
|
-
|
|
898
|
-
const neighbors = edges.get(node);
|
|
899
|
-
if (neighbors) {
|
|
900
|
-
for (const neighbor of neighbors) {
|
|
901
|
-
inDegree[neighbor]--;
|
|
902
|
-
if (inDegree[neighbor] === 0) {
|
|
903
|
-
queue.push(neighbor);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// If there are nodes not in sorted (cycle), append them at the end.
|
|
910
|
-
if (sorted.length < messages.length) {
|
|
911
|
-
const sortedSet = new Set(sorted);
|
|
912
|
-
for (let i = 0; i < messages.length; i++) {
|
|
913
|
-
const entry = byIndex.get(i)!;
|
|
914
|
-
if (!sortedSet.has(entry)) {
|
|
915
|
-
sorted.push(entry);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
return sorted;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* Checks whether a message is an initial RecordsWrite (not an update).
|
|
925
|
-
* An initial write has recordId === message CID context or has no `dateModified` != `dateCreated`.
|
|
926
|
-
*/
|
|
927
|
-
private static isInitialWrite(message: GenericMessage): boolean {
|
|
928
|
-
const desc = message.descriptor as any;
|
|
929
|
-
if (desc.interface !== DwnInterfaceName.Records || desc.method !== DwnMethodName.Write) {
|
|
930
|
-
return false;
|
|
931
|
-
}
|
|
932
|
-
// A RecordsWrite is initial if dateCreated === messageTimestamp (first write for this recordId).
|
|
933
|
-
return desc.dateCreated === desc.messageTimestamp;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
// ---------------------------------------------------------------------------
|
|
937
|
-
// Helpers
|
|
938
|
-
// ---------------------------------------------------------------------------
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* 202: message was successfully written to the remote DWN
|
|
942
|
-
* 204: an initial write message was written without any data
|
|
943
|
-
* 409: message was already present on the remote DWN
|
|
944
|
-
* RecordsDelete + 404: the initial write was not found or already deleted
|
|
945
|
-
*/
|
|
946
|
-
private static syncMessageReplyIsSuccessful(reply: UnionMessageReply): boolean {
|
|
947
|
-
return reply.status.code === 202 ||
|
|
948
|
-
reply.status.code === 204 ||
|
|
949
|
-
reply.status.code === 409 ||
|
|
950
|
-
(
|
|
951
|
-
reply.entry?.message.descriptor.interface === DwnInterfaceName.Records &&
|
|
952
|
-
reply.entry?.message.descriptor.method === DwnMethodName.Delete &&
|
|
953
|
-
reply.status.code === 404
|
|
954
|
-
);
|
|
565
|
+
return topologicalSort(messages);
|
|
955
566
|
}
|
|
956
567
|
|
|
957
568
|
/**
|