@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.
Files changed (64) hide show
  1. package/{readme.md → README.md} +7 -0
  2. package/dist/index.min.js +31 -30
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/cluster/cluster-repo.d.ts +27 -0
  5. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  6. package/dist/src/cluster/cluster-repo.js +139 -18
  7. package/dist/src/cluster/cluster-repo.js.map +1 -1
  8. package/dist/src/cluster/service.d.ts +13 -2
  9. package/dist/src/cluster/service.d.ts.map +1 -1
  10. package/dist/src/cluster/service.js +17 -7
  11. package/dist/src/cluster/service.js.map +1 -1
  12. package/dist/src/index.d.ts +1 -1
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js +1 -1
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/libp2p-node.d.ts +13 -2
  17. package/dist/src/libp2p-node.d.ts.map +1 -1
  18. package/dist/src/libp2p-node.js +35 -16
  19. package/dist/src/libp2p-node.js.map +1 -1
  20. package/dist/src/protocol-client.d.ts.map +1 -1
  21. package/dist/src/protocol-client.js +8 -7
  22. package/dist/src/protocol-client.js.map +1 -1
  23. package/dist/src/repo/cluster-coordinator.d.ts +7 -2
  24. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  25. package/dist/src/repo/cluster-coordinator.js +18 -3
  26. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  27. package/dist/src/repo/coordinator-repo.d.ts +26 -3
  28. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  29. package/dist/src/repo/coordinator-repo.js +117 -22
  30. package/dist/src/repo/coordinator-repo.js.map +1 -1
  31. package/dist/src/repo/service.d.ts +13 -2
  32. package/dist/src/repo/service.d.ts.map +1 -1
  33. package/dist/src/repo/service.js +25 -12
  34. package/dist/src/repo/service.js.map +1 -1
  35. package/dist/src/storage/memory-storage.d.ts +15 -0
  36. package/dist/src/storage/memory-storage.d.ts.map +1 -1
  37. package/dist/src/storage/memory-storage.js +23 -4
  38. package/dist/src/storage/memory-storage.js.map +1 -1
  39. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  40. package/dist/src/storage/storage-repo.js.map +1 -1
  41. package/dist/src/sync/service.d.ts.map +1 -1
  42. package/dist/src/sync/service.js +7 -2
  43. package/dist/src/sync/service.js.map +1 -1
  44. package/package.json +27 -21
  45. package/src/cluster/cluster-repo.ts +836 -711
  46. package/src/cluster/service.ts +44 -31
  47. package/src/index.ts +1 -1
  48. package/src/libp2p-key-network.ts +334 -334
  49. package/src/libp2p-node.ts +371 -339
  50. package/src/network/network-manager-service.ts +334 -334
  51. package/src/protocol-client.ts +53 -54
  52. package/src/repo/client.ts +112 -112
  53. package/src/repo/cluster-coordinator.ts +613 -592
  54. package/src/repo/coordinator-repo.ts +269 -137
  55. package/src/repo/service.ts +237 -219
  56. package/src/storage/block-storage.ts +182 -182
  57. package/src/storage/memory-storage.ts +24 -5
  58. package/src/storage/storage-repo.ts +321 -320
  59. package/src/sync/service.ts +7 -6
  60. package/dist/src/storage/file-storage.d.ts +0 -30
  61. package/dist/src/storage/file-storage.d.ts.map +0 -1
  62. package/dist/src/storage/file-storage.js +0 -127
  63. package/dist/src/storage/file-storage.js.map +0 -1
  64. package/src/storage/file-storage.ts +0 -163
@@ -1,219 +1,237 @@
1
- import { pipe } from 'it-pipe'
2
- import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed'
3
- import type { Startable, Logger, IncomingStreamData } from '@libp2p/interface'
4
- import type { IRepo, RepoMessage } from '@optimystic/db-core'
5
- import { computeResponsibility } from '../routing/responsibility.js'
6
- import { peersEqual } from '../peer-utils.js'
7
- import { sha256 } from 'multiformats/hashes/sha2'
8
- import { buildKnownPeers } from '../routing/libp2p-known-peers.js'
9
- import { encodePeers } from './redirect.js'
10
- import type { Uint8ArrayList } from 'uint8arraylist'
11
-
12
- // Define Components interface
13
- interface BaseComponents {
14
- logger: { forComponent: (name: string) => Logger },
15
- registrar: {
16
- handle: (protocol: string, handler: (data: IncomingStreamData) => void, options: any) => Promise<void>
17
- unhandle: (protocol: string) => Promise<void>
18
- }
19
- }
20
-
21
- export type RepoServiceComponents = BaseComponents & {
22
- repo: IRepo
23
- }
24
-
25
- export type RepoServiceInit = {
26
- protocol?: string,
27
- protocolPrefix?: string,
28
- maxInboundStreams?: number,
29
- maxOutboundStreams?: number,
30
- logPrefix?: string,
31
- kBucketSize?: number,
32
- }
33
-
34
- export function repoService(init: RepoServiceInit = {}): (components: RepoServiceComponents) => RepoService {
35
- return (components: RepoServiceComponents) => new RepoService(components, init);
36
- }
37
-
38
- /**
39
- * A libp2p service that handles repo protocol messages
40
- */
41
- export class RepoService implements Startable {
42
- private readonly protocol: string
43
- private readonly maxInboundStreams: number
44
- private readonly maxOutboundStreams: number
45
- private readonly log: Logger
46
- private readonly repo: IRepo
47
- private readonly components: RepoServiceComponents
48
- private running: boolean
49
- private readonly k: number
50
-
51
- constructor(components: RepoServiceComponents, init: RepoServiceInit = {}) {
52
- this.components = components
53
- const computed = init.protocol ?? (init.protocolPrefix ?? '/db-p2p') + '/repo/1.0.0'
54
- this.protocol = computed
55
- this.maxInboundStreams = init.maxInboundStreams ?? 32
56
- this.maxOutboundStreams = init.maxOutboundStreams ?? 64
57
- this.log = components.logger.forComponent(init.logPrefix ?? 'db-p2p:repo-service')
58
- this.repo = components.repo
59
- this.running = false
60
- this.k = init.kBucketSize ?? 10
61
- }
62
-
63
- readonly [Symbol.toStringTag] = '@libp2p/repo-service'
64
-
65
- /**
66
- * Start the service
67
- */
68
- async start(): Promise<void> {
69
- if (this.running) {
70
- return
71
- }
72
-
73
- await this.components.registrar.handle(this.protocol, this.handleIncomingStream.bind(this), {
74
- maxInboundStreams: this.maxInboundStreams,
75
- maxOutboundStreams: this.maxOutboundStreams
76
- })
77
-
78
- this.running = true
79
- }
80
-
81
- /**
82
- * Stop the service
83
- */
84
- async stop(): Promise<void> {
85
- if (!this.running) {
86
- return
87
- }
88
-
89
- await this.components.registrar.unhandle(this.protocol)
90
- this.running = false
91
- }
92
-
93
- /**
94
- * Handle incoming streams on the repo protocol
95
- */
96
- private handleIncomingStream(data: IncomingStreamData): void {
97
- const { stream, connection } = data
98
- const peerId = connection.remotePeer
99
-
100
- const processStream = async function* (this: RepoService, source: AsyncIterable<Uint8ArrayList>) {
101
- for await (const msg of source) {
102
- // Decode the message
103
- const decoded = new TextDecoder().decode(msg.subarray())
104
- const message = JSON.parse(decoded) as RepoMessage
105
-
106
- // Process each operation
107
- const operation = message.operations[0]
108
- let response: any
109
-
110
- if ('get' in operation) {
111
- {
112
- // Use sha256 digest of block id string for consistent key space
113
- const mh = await sha256.digest(new TextEncoder().encode(operation.get.blockIds[0]!))
114
- const key = mh.digest
115
- const nm: any = (this.components as any).libp2p?.services?.networkManager
116
- if (nm?.getCluster) {
117
- const cluster: any[] = await nm.getCluster(key);
118
- (message as any).cluster = (cluster as any[]).map(p => p.toString?.() ?? String(p))
119
- const selfId = (this.components as any).libp2p.peerId
120
- const isMember = cluster.some((p: any) => peersEqual(p, selfId))
121
- const smallMesh = cluster.length < this.k
122
- if (!smallMesh && !isMember) {
123
- const peers = cluster.filter((p: any) => !peersEqual(p, selfId))
124
- console.debug('repo-service:redirect', {
125
- peerId: selfId.toString(),
126
- reason: 'not-cluster-member',
127
- operation: 'get',
128
- blockId: operation.get.blockIds[0],
129
- cluster: cluster.map((p: any) => p.toString?.() ?? String(p))
130
- })
131
- response = encodePeers(peers.map((pid: any) => ({ id: pid.toString(), addrs: [] })))
132
- } else {
133
- response = await this.repo.get(operation.get, { expiration: message.expiration })
134
- }
135
- } else {
136
- response = await this.repo.get(operation.get, { expiration: message.expiration })
137
- }
138
- }
139
- } else if ('pend' in operation) {
140
- {
141
- const id = Object.keys(operation.pend.transforms)[0]!
142
- const mh = await sha256.digest(new TextEncoder().encode(id))
143
- const key = mh.digest
144
- const nm: any = (this.components as any).libp2p?.services?.networkManager
145
- if (nm?.getCluster) {
146
- const cluster: any[] = await nm.getCluster(key)
147
- ; (message as any).cluster = (cluster as any[]).map(p => p.toString?.() ?? String(p))
148
- const selfId = (this.components as any).libp2p.peerId
149
- const isMember = cluster.some((p: any) => peersEqual(p, selfId))
150
- const smallMesh = cluster.length < this.k
151
- if (!smallMesh && !isMember) {
152
- const peers = cluster.filter((p: any) => !peersEqual(p, selfId))
153
- console.debug('repo-service:redirect', {
154
- peerId: selfId.toString(),
155
- reason: 'not-cluster-member',
156
- operation: 'pend',
157
- blockId: id,
158
- cluster: cluster.map((p: any) => p.toString?.() ?? String(p))
159
- })
160
- response = encodePeers(peers.map((pid: any) => ({ id: pid.toString(), addrs: [] })))
161
- } else {
162
- response = await this.repo.pend(operation.pend, { expiration: message.expiration })
163
- }
164
- } else {
165
- response = await this.repo.pend(operation.pend, { expiration: message.expiration })
166
- }
167
- }
168
- } else if ('cancel' in operation) {
169
- response = await this.repo.cancel(operation.cancel.actionRef, {
170
- expiration: message.expiration
171
- })
172
- } else if ('commit' in operation) {
173
- {
174
- const mh = await sha256.digest(new TextEncoder().encode(operation.commit.tailId))
175
- const key = mh.digest
176
- const nm: any = (this.components as any).libp2p?.services?.networkManager
177
- if (nm?.getCluster) {
178
- const cluster: any[] = await nm.getCluster(key)
179
- ; (message as any).cluster = (cluster as any[]).map(p => p.toString?.() ?? String(p))
180
- const selfId = (this.components as any).libp2p.peerId
181
- const isMember = cluster.some((p: any) => peersEqual(p, selfId))
182
- const smallMesh = cluster.length < this.k
183
- if (!smallMesh && !isMember) {
184
- const peers = cluster.filter((p: any) => !peersEqual(p, selfId))
185
- console.debug('repo-service:redirect', {
186
- peerId: selfId.toString(),
187
- reason: 'not-cluster-member',
188
- operation: 'commit',
189
- tailId: operation.commit.tailId,
190
- cluster: cluster.map((p: any) => p.toString?.() ?? String(p))
191
- })
192
- response = encodePeers(peers.map((pid: any) => ({ id: pid.toString(), addrs: [] })))
193
- } else {
194
- response = await this.repo.commit(operation.commit, { expiration: message.expiration })
195
- }
196
- } else {
197
- response = await this.repo.commit(operation.commit, { expiration: message.expiration })
198
- }
199
- }
200
- }
201
-
202
- // Encode and yield the response
203
- yield new TextEncoder().encode(JSON.stringify(response))
204
- }
205
- }
206
-
207
- Promise.resolve().then(async () => {
208
- await pipe(
209
- stream,
210
- (source) => lpDecode(source),
211
- processStream.bind(this),
212
- (source) => lpEncode(source),
213
- stream
214
- )
215
- }).catch(err => {
216
- this.log.error('error handling repo protocol message from %p - %e', peerId, err)
217
- })
218
- }
219
- }
1
+ import { pipe } from 'it-pipe'
2
+ import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed'
3
+ import type { Startable, Logger, Stream, Connection, StreamHandler } from '@libp2p/interface'
4
+ import type { IRepo, RepoMessage } from '@optimystic/db-core'
5
+ import { peersEqual } from '../peer-utils.js'
6
+ import { sha256 } from 'multiformats/hashes/sha2'
7
+ import { encodePeers } from './redirect.js'
8
+ import type { Uint8ArrayList } from 'uint8arraylist'
9
+
10
+ // Define Components interface
11
+ interface BaseComponents {
12
+ logger: { forComponent: (name: string) => Logger },
13
+ registrar: {
14
+ handle: (protocol: string, handler: StreamHandler, options: any) => Promise<void>
15
+ unhandle: (protocol: string) => Promise<void>
16
+ }
17
+ }
18
+
19
+ export type RepoServiceComponents = BaseComponents & {
20
+ repo: IRepo
21
+ }
22
+
23
+ export type RepoServiceInit = {
24
+ protocol?: string,
25
+ protocolPrefix?: string,
26
+ maxInboundStreams?: number,
27
+ maxOutboundStreams?: number,
28
+ logPrefix?: string,
29
+ kBucketSize?: number,
30
+ /**
31
+ * Responsibility K - the replica set size for determining cluster membership.
32
+ * This is distinct from kBucketSize (DHT routing).
33
+ * When set, this determines how many peers (by XOR distance) are considered
34
+ * responsible for a key. If this node is not in the top responsibilityK peers,
35
+ * it will redirect requests to closer peers.
36
+ * Default: 1 (only the closest peer handles requests)
37
+ */
38
+ responsibilityK?: number,
39
+ }
40
+
41
+ export function repoService(init: RepoServiceInit = {}): (components: RepoServiceComponents) => RepoService {
42
+ return (components: RepoServiceComponents) => new RepoService(components, init);
43
+ }
44
+
45
+ /**
46
+ * A libp2p service that handles repo protocol messages
47
+ */
48
+ export class RepoService implements Startable {
49
+ private readonly protocol: string
50
+ private readonly maxInboundStreams: number
51
+ private readonly maxOutboundStreams: number
52
+ private readonly log: Logger
53
+ private readonly repo: IRepo
54
+ private readonly components: RepoServiceComponents
55
+ private running: boolean
56
+ private readonly k: number
57
+ /** Responsibility K - how many peers are responsible for a key (for redirect decisions) */
58
+ private readonly responsibilityK: number
59
+
60
+ constructor(components: RepoServiceComponents, init: RepoServiceInit = {}) {
61
+ this.components = components
62
+ const computed = init.protocol ?? (init.protocolPrefix ?? '/db-p2p') + '/repo/1.0.0'
63
+ this.protocol = computed
64
+ this.maxInboundStreams = init.maxInboundStreams ?? 32
65
+ this.maxOutboundStreams = init.maxOutboundStreams ?? 64
66
+ this.log = components.logger.forComponent(init.logPrefix ?? 'db-p2p:repo-service')
67
+ this.repo = components.repo
68
+ this.running = false
69
+ this.k = init.kBucketSize ?? 10
70
+ this.responsibilityK = init.responsibilityK ?? 1
71
+ }
72
+
73
+ readonly [Symbol.toStringTag] = '@libp2p/repo-service'
74
+
75
+ /**
76
+ * Start the service
77
+ */
78
+ async start(): Promise<void> {
79
+ if (this.running) {
80
+ return
81
+ }
82
+
83
+ await this.components.registrar.handle(this.protocol, this.handleIncomingStream.bind(this), {
84
+ maxInboundStreams: this.maxInboundStreams,
85
+ maxOutboundStreams: this.maxOutboundStreams
86
+ })
87
+
88
+ this.running = true
89
+ }
90
+
91
+ /**
92
+ * Stop the service
93
+ */
94
+ async stop(): Promise<void> {
95
+ if (!this.running) {
96
+ return
97
+ }
98
+
99
+ await this.components.registrar.unhandle(this.protocol)
100
+ this.running = false
101
+ }
102
+
103
+ /**
104
+ * Handle incoming streams on the repo protocol
105
+ */
106
+ private handleIncomingStream(stream: Stream, connection: Connection): void {
107
+ const peerId = connection.remotePeer
108
+
109
+ const processStream = async function* (this: RepoService, source: AsyncIterable<Uint8ArrayList>) {
110
+ for await (const msg of source) {
111
+ // Decode the message
112
+ const decoded = new TextDecoder().decode(msg.subarray())
113
+ const message = JSON.parse(decoded) as RepoMessage
114
+
115
+ // Process each operation
116
+ const operation = message.operations[0]
117
+ let response: any
118
+
119
+ if ('get' in operation) {
120
+ {
121
+ // Use sha256 digest of block id string for consistent key space
122
+ const mh = await sha256.digest(new TextEncoder().encode(operation.get.blockIds[0]!))
123
+ const key = mh.digest
124
+ const nm: any = (this.components as any).libp2p?.services?.networkManager
125
+ if (nm?.getCluster) {
126
+ const cluster: any[] = await nm.getCluster(key);
127
+ (message as any).cluster = (cluster as any[]).map(p => p.toString?.() ?? String(p))
128
+ const selfId = (this.components as any).libp2p.peerId
129
+ const isMember = cluster.some((p: any) => peersEqual(p, selfId))
130
+ // Use responsibilityK to determine if we're in the responsible set
131
+ const smallMesh = cluster.length < this.responsibilityK
132
+ if (!smallMesh && !isMember) {
133
+ const peers = cluster.filter((p: any) => !peersEqual(p, selfId))
134
+ console.debug('repo-service:redirect', {
135
+ peerId: selfId.toString(),
136
+ reason: 'not-cluster-member',
137
+ operation: 'get',
138
+ blockId: operation.get.blockIds[0],
139
+ cluster: cluster.map((p: any) => p.toString?.() ?? String(p))
140
+ })
141
+ response = encodePeers(peers.map((pid: any) => ({ id: pid.toString(), addrs: [] })))
142
+ } else {
143
+ response = await this.repo.get(operation.get, { expiration: message.expiration, skipClusterFetch: true } as any)
144
+ }
145
+ } else {
146
+ response = await this.repo.get(operation.get, { expiration: message.expiration, skipClusterFetch: true } as any)
147
+ }
148
+ }
149
+ } else if ('pend' in operation) {
150
+ {
151
+ const id = Object.keys(operation.pend.transforms)[0]!
152
+ const mh = await sha256.digest(new TextEncoder().encode(id))
153
+ const key = mh.digest
154
+ const nm: any = (this.components as any).libp2p?.services?.networkManager
155
+ if (nm?.getCluster) {
156
+ const cluster: any[] = await nm.getCluster(key)
157
+ ; (message as any).cluster = (cluster as any[]).map(p => p.toString?.() ?? String(p))
158
+ const selfId = (this.components as any).libp2p.peerId
159
+ const isMember = cluster.some((p: any) => peersEqual(p, selfId))
160
+ // Use responsibilityK to determine if we're in the responsible set
161
+ const smallMesh = cluster.length < this.responsibilityK
162
+ if (!smallMesh && !isMember) {
163
+ const peers = cluster.filter((p: any) => !peersEqual(p, selfId))
164
+ console.debug('repo-service:redirect', {
165
+ peerId: selfId.toString(),
166
+ reason: 'not-cluster-member',
167
+ operation: 'pend',
168
+ blockId: id,
169
+ cluster: cluster.map((p: any) => p.toString?.() ?? String(p))
170
+ })
171
+ response = encodePeers(peers.map((pid: any) => ({ id: pid.toString(), addrs: [] })))
172
+ } else {
173
+ response = await this.repo.pend(operation.pend, { expiration: message.expiration })
174
+ }
175
+ } else {
176
+ response = await this.repo.pend(operation.pend, { expiration: message.expiration })
177
+ }
178
+ }
179
+ } else if ('cancel' in operation) {
180
+ response = await this.repo.cancel(operation.cancel.actionRef, {
181
+ expiration: message.expiration
182
+ })
183
+ } else if ('commit' in operation) {
184
+ {
185
+ const mh = await sha256.digest(new TextEncoder().encode(operation.commit.tailId))
186
+ const key = mh.digest
187
+ const nm: any = (this.components as any).libp2p?.services?.networkManager
188
+ if (nm?.getCluster) {
189
+ const cluster: any[] = await nm.getCluster(key)
190
+ ; (message as any).cluster = (cluster as any[]).map(p => p.toString?.() ?? String(p))
191
+ const selfId = (this.components as any).libp2p.peerId
192
+ const isMember = cluster.some((p: any) => peersEqual(p, selfId))
193
+ // Use responsibilityK to determine if we're in the responsible set
194
+ const smallMesh = cluster.length < this.responsibilityK
195
+ if (!smallMesh && !isMember) {
196
+ const peers = cluster.filter((p: any) => !peersEqual(p, selfId))
197
+ console.debug('repo-service:redirect', {
198
+ peerId: selfId.toString(),
199
+ reason: 'not-cluster-member',
200
+ operation: 'commit',
201
+ tailId: operation.commit.tailId,
202
+ cluster: cluster.map((p: any) => p.toString?.() ?? String(p))
203
+ })
204
+ response = encodePeers(peers.map((pid: any) => ({ id: pid.toString(), addrs: [] })))
205
+ } else {
206
+ response = await this.repo.commit(operation.commit, { expiration: message.expiration })
207
+ }
208
+ } else {
209
+ response = await this.repo.commit(operation.commit, { expiration: message.expiration })
210
+ }
211
+ }
212
+ }
213
+
214
+ // Encode and yield the response
215
+ yield new TextEncoder().encode(JSON.stringify(response))
216
+ }
217
+ }
218
+
219
+ void (async () => {
220
+ try {
221
+ const responses = pipe(
222
+ stream,
223
+ (source) => lpDecode(source),
224
+ processStream.bind(this),
225
+ (source) => lpEncode(source)
226
+ )
227
+ for await (const chunk of responses) {
228
+ stream.send(chunk)
229
+ }
230
+ await stream.close()
231
+ } catch (err) {
232
+ this.log.error('error handling repo protocol message from %p - %e', peerId, err)
233
+ stream.abort(err instanceof Error ? err : new Error(String(err)))
234
+ }
235
+ })()
236
+ }
237
+ }