@optimystic/db-p2p 0.1.1 → 0.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/{readme.md → README.md} +7 -0
- package/dist/index.min.js +31 -30
- package/dist/index.min.js.map +4 -4
- package/dist/src/cluster/cluster-repo.d.ts +27 -0
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +139 -18
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/service.d.ts +13 -2
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +17 -7
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +13 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js +35 -16
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +8 -7
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +7 -2
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +18 -3
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +26 -3
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +117 -22
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts +13 -2
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +25 -12
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/storage/memory-storage.d.ts +15 -0
- package/dist/src/storage/memory-storage.d.ts.map +1 -1
- package/dist/src/storage/memory-storage.js +23 -4
- package/dist/src/storage/memory-storage.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +7 -2
- package/dist/src/sync/service.js.map +1 -1
- package/package.json +27 -21
- package/src/cluster/cluster-repo.ts +836 -711
- package/src/cluster/service.ts +44 -31
- package/src/index.ts +1 -1
- package/src/libp2p-key-network.ts +334 -334
- package/src/libp2p-node.ts +371 -339
- package/src/network/network-manager-service.ts +334 -334
- package/src/protocol-client.ts +53 -54
- package/src/repo/client.ts +112 -112
- package/src/repo/cluster-coordinator.ts +613 -592
- package/src/repo/coordinator-repo.ts +269 -137
- package/src/repo/service.ts +237 -219
- package/src/storage/block-storage.ts +182 -182
- package/src/storage/memory-storage.ts +24 -5
- package/src/storage/storage-repo.ts +321 -320
- package/src/sync/service.ts +7 -6
- package/dist/src/storage/file-storage.d.ts +0 -30
- package/dist/src/storage/file-storage.d.ts.map +0 -1
- package/dist/src/storage/file-storage.js +0 -127
- package/dist/src/storage/file-storage.js.map +0 -1
- package/src/storage/file-storage.ts +0 -163
package/src/protocol-client.ts
CHANGED
|
@@ -1,54 +1,53 @@
|
|
|
1
|
-
import { pipe } from 'it-pipe';
|
|
2
|
-
import { encode as lpEncode, decode as lpDecode } from 'it-length-prefixed';
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
protected readonly
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
1
|
+
import { pipe } from 'it-pipe';
|
|
2
|
+
import { encode as lpEncode, decode as lpDecode } from 'it-length-prefixed';
|
|
3
|
+
import type { PeerId } from '@libp2p/interface';
|
|
4
|
+
import type { IPeerNetwork } from '@optimystic/db-core';
|
|
5
|
+
import { first } from './it-utility.js';
|
|
6
|
+
|
|
7
|
+
/** Base class for clients that communicate via a libp2p protocol */
|
|
8
|
+
export class ProtocolClient {
|
|
9
|
+
constructor(
|
|
10
|
+
protected readonly peerId: PeerId,
|
|
11
|
+
protected readonly peerNetwork: IPeerNetwork,
|
|
12
|
+
) { }
|
|
13
|
+
|
|
14
|
+
protected async processMessage<T>(
|
|
15
|
+
message: unknown,
|
|
16
|
+
protocol: string,
|
|
17
|
+
options?: { signal?: AbortSignal }
|
|
18
|
+
): Promise<T> {
|
|
19
|
+
const stream = await this.peerNetwork.connect(
|
|
20
|
+
this.peerId,
|
|
21
|
+
protocol,
|
|
22
|
+
{ signal: options?.signal }
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Send the request using length-prefixed encoding
|
|
27
|
+
const encoded = pipe(
|
|
28
|
+
[new TextEncoder().encode(JSON.stringify(message))],
|
|
29
|
+
lpEncode
|
|
30
|
+
);
|
|
31
|
+
for await (const chunk of encoded) {
|
|
32
|
+
stream.send(chunk);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Read the response from the stream (which is now directly AsyncIterable)
|
|
36
|
+
const source = pipe(
|
|
37
|
+
stream,
|
|
38
|
+
lpDecode,
|
|
39
|
+
async function* (source) {
|
|
40
|
+
for await (const data of source) {
|
|
41
|
+
const decoded = new TextDecoder().decode(data.subarray());
|
|
42
|
+
const parsed = JSON.parse(decoded);
|
|
43
|
+
yield parsed;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
) as AsyncIterable<T>;
|
|
47
|
+
|
|
48
|
+
return await first(() => source, () => { throw new Error('No response received') });
|
|
49
|
+
} finally {
|
|
50
|
+
await stream.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/repo/client.ts
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IRepo, GetBlockResults, PendSuccess, StaleFailure, ActionBlocks, MessageOptions, CommitResult,
|
|
3
|
-
PendRequest, CommitRequest, BlockGets, IPeerNetwork
|
|
4
|
-
} from "@optimystic/db-core";
|
|
5
|
-
import type { RepoMessage } from "@optimystic/db-core";
|
|
6
|
-
import type { PeerId } from "@libp2p/interface";
|
|
7
|
-
import { ProtocolClient } from "../protocol-client.js";
|
|
8
|
-
import { peerIdFromString } from "@libp2p/peer-id";
|
|
9
|
-
|
|
10
|
-
export class RepoClient extends ProtocolClient implements IRepo {
|
|
11
|
-
private constructor(peerId: PeerId, peerNetwork: IPeerNetwork, readonly protocolPrefix?: string) {
|
|
12
|
-
super(peerId, peerNetwork);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Create a new client instance */
|
|
16
|
-
public static create(peerId: PeerId, peerNetwork: IPeerNetwork, protocolPrefix?: string): RepoClient {
|
|
17
|
-
return new RepoClient(peerId, peerNetwork, protocolPrefix);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async get(blockGets: BlockGets, options: MessageOptions): Promise<GetBlockResults> {
|
|
21
|
-
return this.processRepoMessage<GetBlockResults>(
|
|
22
|
-
[{ get: blockGets }],
|
|
23
|
-
options
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async pend(request: PendRequest, options: MessageOptions): Promise<PendSuccess | StaleFailure> {
|
|
28
|
-
return this.processRepoMessage<PendSuccess | StaleFailure>(
|
|
29
|
-
[{ pend: request }],
|
|
30
|
-
options
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async cancel(actionRef: ActionBlocks, options: MessageOptions): Promise<void> {
|
|
35
|
-
return this.processRepoMessage<void>(
|
|
36
|
-
[{ cancel: { actionRef } }],
|
|
37
|
-
options
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async commit(request: CommitRequest, options: MessageOptions): Promise<CommitResult> {
|
|
42
|
-
return this.processRepoMessage<CommitResult>(
|
|
43
|
-
[{ commit: request }],
|
|
44
|
-
options
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private async processRepoMessage<T>(
|
|
49
|
-
operations: RepoMessage['operations'],
|
|
50
|
-
options: MessageOptions,
|
|
51
|
-
hop: number = 0
|
|
52
|
-
): Promise<T> {
|
|
53
|
-
const message: RepoMessage = {
|
|
54
|
-
operations,
|
|
55
|
-
expiration: options.expiration,
|
|
56
|
-
};
|
|
57
|
-
const deadline = options.expiration ?? (Date.now() + 30_000)
|
|
58
|
-
const msLeft = Math.max(1, deadline - Date.now())
|
|
59
|
-
const withTimeout = async <U>(fn: () => Promise<U>): Promise<U> => {
|
|
60
|
-
return await Promise.race<U>([
|
|
61
|
-
fn(),
|
|
62
|
-
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('RepoClient timeout')), msLeft))
|
|
63
|
-
])
|
|
64
|
-
}
|
|
65
|
-
let response: any
|
|
66
|
-
const preferred = (this.protocolPrefix ?? '/db-p2p') + '/repo/1.0.0'
|
|
67
|
-
response = await withTimeout(() => super.processMessage<any>(message, preferred, { signal: options?.signal }))
|
|
68
|
-
|
|
69
|
-
if (response?.redirect?.peers?.length) {
|
|
70
|
-
if (hop >= 2) {
|
|
71
|
-
throw new Error('Redirect loop detected in RepoClient (max hops reached)')
|
|
72
|
-
}
|
|
73
|
-
const currentIdStr = this.peerId.toString()
|
|
74
|
-
const next = response.redirect.peers.find((p: any) => p.id !== currentIdStr) ?? response.redirect.peers[0]
|
|
75
|
-
const nextId = peerIdFromString(next.id)
|
|
76
|
-
if (next.id === currentIdStr) {
|
|
77
|
-
throw new Error('Redirect loop detected in RepoClient (same peer)')
|
|
78
|
-
}
|
|
79
|
-
// cache hint
|
|
80
|
-
this.recordCoordinatorForOpsIfSupported(operations, nextId)
|
|
81
|
-
// single-hop retry against target peer using repo protocol
|
|
82
|
-
const nextClient = RepoClient.create(nextId, this.peerNetwork, this.protocolPrefix)
|
|
83
|
-
return await nextClient.processRepoMessage<T>(operations, options, hop + 1)
|
|
84
|
-
}
|
|
85
|
-
return response as T;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private extractKeyFromOperations(ops: RepoMessage['operations']): Uint8Array | undefined {
|
|
89
|
-
const op = ops[0];
|
|
90
|
-
if ('get' in op) {
|
|
91
|
-
const id = op.get.blockIds[0];
|
|
92
|
-
return id ? new TextEncoder().encode(id) : undefined;
|
|
93
|
-
}
|
|
94
|
-
if ('pend' in op) {
|
|
95
|
-
const id = Object.keys(op.pend.transforms)[0];
|
|
96
|
-
return id ? new TextEncoder().encode(id) : undefined;
|
|
97
|
-
}
|
|
98
|
-
if ('commit' in op) {
|
|
99
|
-
return new TextEncoder().encode(op.commit.tailId);
|
|
100
|
-
}
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private recordCoordinatorForOpsIfSupported(ops: RepoMessage['operations'], peerId: PeerId): void {
|
|
105
|
-
const keyBytes = this.extractKeyFromOperations(ops)
|
|
106
|
-
const pn: any = this.peerNetwork as any
|
|
107
|
-
if (keyBytes != null && typeof pn?.recordCoordinator === 'function') {
|
|
108
|
-
pn.recordCoordinator(keyBytes, peerId)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
IRepo, GetBlockResults, PendSuccess, StaleFailure, ActionBlocks, MessageOptions, CommitResult,
|
|
3
|
+
PendRequest, CommitRequest, BlockGets, IPeerNetwork
|
|
4
|
+
} from "@optimystic/db-core";
|
|
5
|
+
import type { RepoMessage } from "@optimystic/db-core";
|
|
6
|
+
import type { PeerId } from "@libp2p/interface";
|
|
7
|
+
import { ProtocolClient } from "../protocol-client.js";
|
|
8
|
+
import { peerIdFromString } from "@libp2p/peer-id";
|
|
9
|
+
|
|
10
|
+
export class RepoClient extends ProtocolClient implements IRepo {
|
|
11
|
+
private constructor(peerId: PeerId, peerNetwork: IPeerNetwork, readonly protocolPrefix?: string) {
|
|
12
|
+
super(peerId, peerNetwork);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Create a new client instance */
|
|
16
|
+
public static create(peerId: PeerId, peerNetwork: IPeerNetwork, protocolPrefix?: string): RepoClient {
|
|
17
|
+
return new RepoClient(peerId, peerNetwork, protocolPrefix);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async get(blockGets: BlockGets, options: MessageOptions): Promise<GetBlockResults> {
|
|
21
|
+
return this.processRepoMessage<GetBlockResults>(
|
|
22
|
+
[{ get: blockGets }],
|
|
23
|
+
options
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async pend(request: PendRequest, options: MessageOptions): Promise<PendSuccess | StaleFailure> {
|
|
28
|
+
return this.processRepoMessage<PendSuccess | StaleFailure>(
|
|
29
|
+
[{ pend: request }],
|
|
30
|
+
options
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async cancel(actionRef: ActionBlocks, options: MessageOptions): Promise<void> {
|
|
35
|
+
return this.processRepoMessage<void>(
|
|
36
|
+
[{ cancel: { actionRef } }],
|
|
37
|
+
options
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async commit(request: CommitRequest, options: MessageOptions): Promise<CommitResult> {
|
|
42
|
+
return this.processRepoMessage<CommitResult>(
|
|
43
|
+
[{ commit: request }],
|
|
44
|
+
options
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async processRepoMessage<T>(
|
|
49
|
+
operations: RepoMessage['operations'],
|
|
50
|
+
options: MessageOptions,
|
|
51
|
+
hop: number = 0
|
|
52
|
+
): Promise<T> {
|
|
53
|
+
const message: RepoMessage = {
|
|
54
|
+
operations,
|
|
55
|
+
expiration: options.expiration,
|
|
56
|
+
};
|
|
57
|
+
const deadline = options.expiration ?? (Date.now() + 30_000)
|
|
58
|
+
const msLeft = Math.max(1, deadline - Date.now())
|
|
59
|
+
const withTimeout = async <U>(fn: () => Promise<U>): Promise<U> => {
|
|
60
|
+
return await Promise.race<U>([
|
|
61
|
+
fn(),
|
|
62
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('RepoClient timeout')), msLeft))
|
|
63
|
+
])
|
|
64
|
+
}
|
|
65
|
+
let response: any
|
|
66
|
+
const preferred = (this.protocolPrefix ?? '/db-p2p') + '/repo/1.0.0'
|
|
67
|
+
response = await withTimeout(() => super.processMessage<any>(message, preferred, { signal: options?.signal }))
|
|
68
|
+
|
|
69
|
+
if (response?.redirect?.peers?.length) {
|
|
70
|
+
if (hop >= 2) {
|
|
71
|
+
throw new Error('Redirect loop detected in RepoClient (max hops reached)')
|
|
72
|
+
}
|
|
73
|
+
const currentIdStr = this.peerId.toString()
|
|
74
|
+
const next = response.redirect.peers.find((p: any) => p.id !== currentIdStr) ?? response.redirect.peers[0]
|
|
75
|
+
const nextId = peerIdFromString(next.id)
|
|
76
|
+
if (next.id === currentIdStr) {
|
|
77
|
+
throw new Error('Redirect loop detected in RepoClient (same peer)')
|
|
78
|
+
}
|
|
79
|
+
// cache hint
|
|
80
|
+
this.recordCoordinatorForOpsIfSupported(operations, nextId)
|
|
81
|
+
// single-hop retry against target peer using repo protocol
|
|
82
|
+
const nextClient = RepoClient.create(nextId, this.peerNetwork, this.protocolPrefix)
|
|
83
|
+
return await nextClient.processRepoMessage<T>(operations, options, hop + 1)
|
|
84
|
+
}
|
|
85
|
+
return response as T;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private extractKeyFromOperations(ops: RepoMessage['operations']): Uint8Array | undefined {
|
|
89
|
+
const op = ops[0];
|
|
90
|
+
if ('get' in op) {
|
|
91
|
+
const id = op.get.blockIds[0];
|
|
92
|
+
return id ? new TextEncoder().encode(id) : undefined;
|
|
93
|
+
}
|
|
94
|
+
if ('pend' in op) {
|
|
95
|
+
const id = Object.keys(op.pend.transforms)[0];
|
|
96
|
+
return id ? new TextEncoder().encode(id) : undefined;
|
|
97
|
+
}
|
|
98
|
+
if ('commit' in op) {
|
|
99
|
+
return new TextEncoder().encode(op.commit.tailId);
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private recordCoordinatorForOpsIfSupported(ops: RepoMessage['operations'], peerId: PeerId): void {
|
|
105
|
+
const keyBytes = this.extractKeyFromOperations(ops)
|
|
106
|
+
const pn: any = this.peerNetwork as any
|
|
107
|
+
if (keyBytes != null && typeof pn?.recordCoordinator === 'function') {
|
|
108
|
+
pn.recordCoordinator(keyBytes, peerId)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
}
|