@filoz/repair-cli 0.4.0 → 1.1.0
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/package.json +4 -4
- package/dist/src/api.d.ts +35 -0
- package/dist/src/api.d.ts.map +1 -0
- package/dist/src/api.js +118 -0
- package/dist/src/api.js.map +1 -0
- package/dist/src/commands/datasets.d.ts +5 -7
- package/dist/src/commands/datasets.d.ts.map +1 -1
- package/dist/src/commands/datasets.js +33 -35
- package/dist/src/commands/datasets.js.map +1 -1
- package/dist/src/commands/providers.d.ts +5 -7
- package/dist/src/commands/providers.d.ts.map +1 -1
- package/dist/src/commands/providers.js +32 -33
- package/dist/src/commands/providers.js.map +1 -1
- package/dist/src/commands/repair.d.ts +5 -7
- package/dist/src/commands/repair.d.ts.map +1 -1
- package/dist/src/commands/repair.js +5 -1
- package/dist/src/commands/repair.js.map +1 -1
- package/dist/src/commands/replicate.d.ts +5 -7
- package/dist/src/commands/replicate.d.ts.map +1 -1
- package/dist/src/commands/replicate.js +5 -1
- package/dist/src/commands/replicate.js.map +1 -1
- package/dist/src/commands/second-copy.d.ts +5 -7
- package/dist/src/commands/second-copy.d.ts.map +1 -1
- package/dist/src/commands/second-copy.js +5 -1
- package/dist/src/commands/second-copy.js.map +1 -1
- package/dist/src/commands/session-key.d.ts +5 -7
- package/dist/src/commands/session-key.d.ts.map +1 -1
- package/dist/src/commands/setup.d.ts.map +1 -1
- package/dist/src/commands/setup.js +11 -32
- package/dist/src/commands/setup.js.map +1 -1
- package/dist/src/commands/wallet.d.ts +5 -7
- package/dist/src/commands/wallet.d.ts.map +1 -1
- package/dist/src/db/dedupe-cids.d.ts +6 -7
- package/dist/src/db/dedupe-cids.d.ts.map +1 -1
- package/dist/src/db/dedupe-cids.js +2 -14
- package/dist/src/db/dedupe-cids.js.map +1 -1
- package/dist/src/db/find-repair-dataset.d.ts +3 -3
- package/dist/src/db/find-repair-dataset.d.ts.map +1 -1
- package/dist/src/db/find-repair-dataset.js +16 -14
- package/dist/src/db/find-repair-dataset.js.map +1 -1
- package/dist/src/db/get-active-data-set-ids.d.ts +9 -0
- package/dist/src/db/get-active-data-set-ids.d.ts.map +1 -0
- package/dist/src/db/get-active-data-set-ids.js +9 -0
- package/dist/src/db/get-active-data-set-ids.js.map +1 -0
- package/dist/src/db/get-alternate-provider-urls.d.ts +6 -0
- package/dist/src/db/get-alternate-provider-urls.d.ts.map +1 -0
- package/dist/src/db/get-alternate-provider-urls.js +24 -0
- package/dist/src/db/get-alternate-provider-urls.js.map +1 -0
- package/dist/src/db/get-pieces.d.ts +8 -27
- package/dist/src/db/get-pieces.d.ts.map +1 -1
- package/dist/src/db/get-pieces.js +44 -97
- package/dist/src/db/get-pieces.js.map +1 -1
- package/dist/src/db/get-repair-provider.d.ts +2 -2
- package/dist/src/db/get-repair-provider.d.ts.map +1 -1
- package/dist/src/db/get-repair-provider.js +8 -20
- package/dist/src/db/get-repair-provider.js.map +1 -1
- package/dist/src/db/repair-create.js +3 -3
- package/dist/src/db/repair-create.js.map +1 -1
- package/dist/src/db/replicate-create.d.ts.map +1 -1
- package/dist/src/db/replicate-create.js +13 -21
- package/dist/src/db/replicate-create.js.map +1 -1
- package/dist/src/db/second-copy-create.js +2 -2
- package/dist/src/db/second-copy-create.js.map +1 -1
- package/dist/src/local-schema.d.ts +1 -1
- package/dist/src/middleware.d.ts +12 -16
- package/dist/src/middleware.d.ts.map +1 -1
- package/dist/src/middleware.js +6 -10
- package/dist/src/middleware.js.map +1 -1
- package/dist/src/pipeline/add-pieces.d.ts +4 -4
- package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
- package/dist/src/pipeline/add-pieces.js +32 -19
- package/dist/src/pipeline/add-pieces.js.map +1 -1
- package/dist/src/pipeline/create-datasets.d.ts +4 -4
- package/dist/src/pipeline/create-datasets.d.ts.map +1 -1
- package/dist/src/pipeline/create-datasets.js +7 -14
- package/dist/src/pipeline/create-datasets.js.map +1 -1
- package/dist/src/types.d.ts +8 -9
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils.d.ts +45 -37
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +47 -2
- package/dist/src/utils.js.map +1 -1
- package/package.json +4 -4
- package/readme.md +22 -19
- package/src/api.ts +220 -0
- package/src/commands/datasets.ts +33 -38
- package/src/commands/providers.ts +34 -42
- package/src/commands/repair.ts +5 -1
- package/src/commands/replicate.ts +5 -1
- package/src/commands/second-copy.ts +5 -1
- package/src/commands/setup.ts +12 -35
- package/src/db/dedupe-cids.ts +7 -24
- package/src/db/find-repair-dataset.ts +17 -28
- package/src/db/get-active-data-set-ids.ts +29 -0
- package/src/db/get-alternate-provider-urls.ts +43 -0
- package/src/db/get-pieces.ts +82 -214
- package/src/db/get-repair-provider.ts +10 -30
- package/src/db/repair-create.ts +3 -3
- package/src/db/replicate-create.ts +13 -21
- package/src/db/second-copy-create.ts +2 -2
- package/src/middleware.ts +10 -14
- package/src/pipeline/add-pieces.ts +52 -24
- package/src/pipeline/create-datasets.ts +9 -29
- package/src/types.ts +11 -9
- package/src/utils.ts +71 -7
- package/dist/src/db/find-providers-by-cid.d.ts +0 -9
- package/dist/src/db/find-providers-by-cid.d.ts.map +0 -1
- package/dist/src/db/find-providers-by-cid.js +0 -43
- package/dist/src/db/find-providers-by-cid.js.map +0 -1
- package/dist/src/indexer-schema.d.ts +0 -549
- package/dist/src/indexer-schema.d.ts.map +0 -1
- package/dist/src/indexer-schema.js +0 -56
- package/dist/src/indexer-schema.js.map +0 -1
- package/src/db/find-providers-by-cid.ts +0 -70
- package/src/indexer-schema.ts +0 -77
package/src/commands/setup.ts
CHANGED
|
@@ -3,21 +3,16 @@ import { Cli, z } from 'incur'
|
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import type { Hash } from 'viem'
|
|
5
5
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
6
|
+
import { DEFAULT_API_URL } from '../api.ts'
|
|
6
7
|
import { config, createLocalDatabase, globalOptions, migrateLocalDatabase } from '../utils.ts'
|
|
7
8
|
|
|
8
|
-
function
|
|
9
|
-
let url: URL
|
|
9
|
+
function validateHttpUrl(value: string) {
|
|
10
10
|
try {
|
|
11
|
-
url = new URL(value)
|
|
11
|
+
const url = new URL(value)
|
|
12
|
+
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
12
13
|
} catch {
|
|
13
14
|
return false
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
if (url.protocol !== 'postgresql:' && url.protocol !== 'postgres:') {
|
|
17
|
-
return false
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return Boolean(url.hostname && url.username && url.password && url.pathname.length > 1)
|
|
21
16
|
}
|
|
22
17
|
|
|
23
18
|
export const setup = Cli.create('setup', {
|
|
@@ -44,35 +39,18 @@ export const setup = Cli.create('setup', {
|
|
|
44
39
|
})
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
message: 'Enter
|
|
50
|
-
validate(value) {
|
|
51
|
-
if (!value || !validatePostgresUrl(value)) {
|
|
52
|
-
return `Invalid postgres URL!`
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
initialValue: config.get('indexerMainnetUrl'),
|
|
56
|
-
withGuide: false,
|
|
57
|
-
})
|
|
58
|
-
if (p.isCancel(indexerMainnetUrl)) {
|
|
59
|
-
return c.error({
|
|
60
|
-
code: 'SETUP_CANCELLED',
|
|
61
|
-
message: 'Setup cancelled',
|
|
62
|
-
retryable: false,
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
const indexerCalibrationUrl = await p.text({
|
|
66
|
-
message: 'Enter your Calibration Indexer Postgres URL',
|
|
42
|
+
// API URL
|
|
43
|
+
const apiUrl = await p.text({
|
|
44
|
+
message: 'Enter the Filecoin API URL',
|
|
67
45
|
validate(value) {
|
|
68
|
-
if (!value || !
|
|
69
|
-
return `Invalid
|
|
46
|
+
if (!value || !validateHttpUrl(value)) {
|
|
47
|
+
return `Invalid http(s) URL!`
|
|
70
48
|
}
|
|
71
49
|
},
|
|
72
|
-
initialValue: config.get('
|
|
50
|
+
initialValue: config.get('apiUrl') ?? DEFAULT_API_URL,
|
|
73
51
|
withGuide: false,
|
|
74
52
|
})
|
|
75
|
-
if (p.isCancel(
|
|
53
|
+
if (p.isCancel(apiUrl)) {
|
|
76
54
|
return c.error({
|
|
77
55
|
code: 'SETUP_CANCELLED',
|
|
78
56
|
message: 'Setup cancelled',
|
|
@@ -129,8 +107,7 @@ export const setup = Cli.create('setup', {
|
|
|
129
107
|
|
|
130
108
|
// Set config
|
|
131
109
|
config.set('privateKey', pk)
|
|
132
|
-
config.set('
|
|
133
|
-
config.set('indexerCalibrationUrl', indexerCalibrationUrl)
|
|
110
|
+
config.set('apiUrl', apiUrl)
|
|
134
111
|
config.set('chainId', chainId)
|
|
135
112
|
config.set('dbPath', dbPath)
|
|
136
113
|
config.set('source', source)
|
package/src/db/dedupe-cids.ts
CHANGED
|
@@ -1,37 +1,20 @@
|
|
|
1
|
-
import { and, eq, inArray } from 'drizzle-orm'
|
|
2
1
|
import type { OperationSelect } from '../local-schema.ts'
|
|
3
|
-
import type {
|
|
2
|
+
import type { LocalDatabase } from '../types.ts'
|
|
4
3
|
import { upsertOperations } from './upsert-operations.ts'
|
|
5
4
|
|
|
6
|
-
export type
|
|
7
|
-
indexerDb: IndexerDatabase
|
|
5
|
+
export type DedupeCidsOptions = {
|
|
8
6
|
localDb: LocalDatabase
|
|
9
|
-
|
|
7
|
+
/** CIDs already in the target repair dataset. */
|
|
8
|
+
existingCids: ReadonlySet<string>
|
|
10
9
|
operations: OperationSelect[]
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
13
|
+
* Mark operations whose CID is already in the target repair dataset as completed.
|
|
15
14
|
* Returns operations that are not onchain.
|
|
16
15
|
*/
|
|
17
|
-
export async function dedupeCids({
|
|
18
|
-
const
|
|
19
|
-
const schema = indexerDb._.fullSchema
|
|
20
|
-
|
|
21
|
-
const rows = await indexerDb
|
|
22
|
-
.select({ cid: schema.pieces.cid })
|
|
23
|
-
.from(schema.pieces)
|
|
24
|
-
.where(
|
|
25
|
-
and(eq(schema.pieces.dataSetId, dataSetId), eq(schema.pieces.removed, false), inArray(schema.pieces.cid, cids))
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
const existingCids = new Set<string>()
|
|
29
|
-
const completedOperations: OperationSelect[] = []
|
|
30
|
-
|
|
31
|
-
for (const row of rows) {
|
|
32
|
-
completedOperations.push(...operations.filter((operation) => operation.cid === row.cid))
|
|
33
|
-
existingCids.add(row.cid)
|
|
34
|
-
}
|
|
16
|
+
export async function dedupeCids({ localDb, existingCids, operations }: DedupeCidsOptions) {
|
|
17
|
+
const completedOperations = operations.filter((operation) => existingCids.has(operation.cid))
|
|
35
18
|
|
|
36
19
|
if (completedOperations.length > 0) {
|
|
37
20
|
await upsertOperations({
|
|
@@ -1,49 +1,38 @@
|
|
|
1
|
-
import { and, asc, eq, isNull } from 'drizzle-orm'
|
|
2
1
|
import type { Address } from 'viem'
|
|
3
|
-
import
|
|
2
|
+
import { listDataSets } from '../api.ts'
|
|
3
|
+
import type { IndexerApi } from '../types.ts'
|
|
4
4
|
|
|
5
5
|
export type FindRepairDatasetOptions = {
|
|
6
|
-
|
|
6
|
+
api: IndexerApi
|
|
7
7
|
providerId: bigint
|
|
8
8
|
payer: Address
|
|
9
9
|
source: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Find one IPFS-enabled repair dataset for a provider
|
|
13
|
+
* Find one live IPFS-enabled repair dataset for a provider, if it exists.
|
|
14
14
|
*
|
|
15
15
|
* When multiple datasets match, the lowest `dataSetId` is returned.
|
|
16
16
|
*/
|
|
17
17
|
export async function findRepairDataset({
|
|
18
|
-
|
|
18
|
+
api,
|
|
19
19
|
providerId,
|
|
20
20
|
payer,
|
|
21
21
|
source,
|
|
22
22
|
}: FindRepairDatasetOptions): Promise<bigint | null> {
|
|
23
|
-
const
|
|
23
|
+
const dataSets = await listDataSets(api, {
|
|
24
|
+
owner: payer.toLowerCase(),
|
|
25
|
+
providerId,
|
|
26
|
+
deleted: false,
|
|
27
|
+
withCdn: false,
|
|
28
|
+
})
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.where(
|
|
31
|
-
and(
|
|
32
|
-
eq(schema.dataSets.providerId, providerId),
|
|
33
|
-
eq(schema.dataSets.deleted, false),
|
|
34
|
-
isNull(schema.dataSets.pdpEndEpoch),
|
|
35
|
-
eq(schema.dataSets.payer, payer.toLowerCase()),
|
|
36
|
-
eq(schema.dataSets.source, source),
|
|
37
|
-
eq(schema.dataSets.withCdn, false),
|
|
38
|
-
eq(schema.dataSets.withIpfsIndexing, true)
|
|
39
|
-
)
|
|
40
|
-
)
|
|
41
|
-
.orderBy(asc(schema.dataSets.dataSetId))
|
|
42
|
-
.limit(1)
|
|
43
|
-
|
|
44
|
-
if (result.length === 0) {
|
|
45
|
-
return null
|
|
30
|
+
let dataSetId: bigint | null = null
|
|
31
|
+
for (const dataSet of dataSets) {
|
|
32
|
+
if (dataSet.pdpEndEpoch != null || dataSet.source !== source || !dataSet.withIpfsIndexing) continue
|
|
33
|
+
const id = BigInt(dataSet.dataSetId)
|
|
34
|
+
if (dataSetId == null || id < dataSetId) dataSetId = id
|
|
46
35
|
}
|
|
47
36
|
|
|
48
|
-
return
|
|
37
|
+
return dataSetId
|
|
49
38
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type DataSet, listDataSets } from '../api.ts'
|
|
2
|
+
import type { IndexerApi } from '../types.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether a data set's pieces count at the repair block: not deleted, and either not terminated
|
|
6
|
+
* (no PDP end epoch) or terminated with an end epoch after `blockNumber`, so the provider still
|
|
7
|
+
* has to prove it.
|
|
8
|
+
*/
|
|
9
|
+
export function isDataSetActiveAt(dataSet: Pick<DataSet, 'deleted' | 'pdpEndEpoch'>, blockNumber: bigint): boolean {
|
|
10
|
+
return !dataSet.deleted && (dataSet.pdpEndEpoch == null || BigInt(dataSet.pdpEndEpoch) > blockNumber)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* IDs of a provider's data sets that are active at the repair block (see {@link isDataSetActiveAt}).
|
|
15
|
+
*/
|
|
16
|
+
export async function getActiveDataSetIds({
|
|
17
|
+
api,
|
|
18
|
+
providerId,
|
|
19
|
+
blockNumber,
|
|
20
|
+
}: {
|
|
21
|
+
api: IndexerApi
|
|
22
|
+
providerId: bigint
|
|
23
|
+
blockNumber: bigint
|
|
24
|
+
}): Promise<Set<string>> {
|
|
25
|
+
const dataSets = await listDataSets(api, { providerId, deleted: false })
|
|
26
|
+
return new Set(
|
|
27
|
+
dataSets.filter((dataSet) => isDataSetActiveAt(dataSet, blockNumber)).map((dataSet) => dataSet.dataSetId)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { listProviders, type Provider } from '../api.ts'
|
|
2
|
+
import type { IndexerApi } from '../types.ts'
|
|
3
|
+
|
|
4
|
+
/** Rank endorsed providers first, then approved, then by ascending ID. */
|
|
5
|
+
function compareProviders(a: Provider, b: Provider): number {
|
|
6
|
+
if (a.endorsed !== b.endorsed) return a.endorsed ? -1 : 1
|
|
7
|
+
if (a.approved !== b.approved) return a.approved ? -1 : 1
|
|
8
|
+
const x = BigInt(a.providerId)
|
|
9
|
+
const y = BigInt(b.providerId)
|
|
10
|
+
return x < y ? -1 : x > y ? 1 : 0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Service URLs of the providers to probe for a copy of a repaired piece: approved or endorsed
|
|
15
|
+
* providers with a service URL, then the provider being repaired as a last resort, so pieces only
|
|
16
|
+
* it stores can still be pulled while it serves them.
|
|
17
|
+
*
|
|
18
|
+
* Callers probe these with `findPieceOnProviders`, which keeps this order, so endorsed providers
|
|
19
|
+
* are preferred as pull sources and the repaired provider is used only when no other one serves
|
|
20
|
+
* the piece.
|
|
21
|
+
*/
|
|
22
|
+
export async function getAlternateProviderUrls({
|
|
23
|
+
api,
|
|
24
|
+
repairProviderId,
|
|
25
|
+
}: {
|
|
26
|
+
api: IndexerApi
|
|
27
|
+
repairProviderId: bigint
|
|
28
|
+
}): Promise<string[]> {
|
|
29
|
+
const providers = await listProviders(api)
|
|
30
|
+
const repairProviderUrl = providers.find((provider) => BigInt(provider.providerId) === repairProviderId)?.serviceUrl
|
|
31
|
+
const urls = providers
|
|
32
|
+
.filter(
|
|
33
|
+
(provider) =>
|
|
34
|
+
(provider.approved || provider.endorsed) &&
|
|
35
|
+
provider.serviceUrl &&
|
|
36
|
+
BigInt(provider.providerId) !== repairProviderId
|
|
37
|
+
)
|
|
38
|
+
.sort(compareProviders)
|
|
39
|
+
.map((provider) => provider.serviceUrl as string)
|
|
40
|
+
|
|
41
|
+
if (repairProviderUrl) urls.push(repairProviderUrl)
|
|
42
|
+
return urls
|
|
43
|
+
}
|
package/src/db/get-pieces.ts
CHANGED
|
@@ -1,157 +1,70 @@
|
|
|
1
1
|
import * as Piece from '@filoz/synapse-core/piece'
|
|
2
|
-
import { and, asc, eq, isNull, lte, or } from 'drizzle-orm'
|
|
3
2
|
import pMap from 'p-map'
|
|
3
|
+
import { listDataSetPieces, MAX_PAGE_SIZE, providerPiecePages, toStringMetadata } from '../api.ts'
|
|
4
4
|
import type { OperationInsert } from '../local-schema.ts'
|
|
5
|
-
import type {
|
|
6
|
-
import {
|
|
5
|
+
import type { IndexerApi } from '../types.ts'
|
|
6
|
+
import { getActiveDataSetIds } from './get-active-data-set-ids.ts'
|
|
7
|
+
import { getAlternateProviderUrls } from './get-alternate-provider-urls.ts'
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
/** Options for fetching one page of `add_piece` operations for a repair. */
|
|
12
|
-
export type GetPiecesPageOptions = {
|
|
13
|
-
indexerDb: IndexerDatabase
|
|
9
|
+
/** Options for walking `add_piece` operations for a repair. */
|
|
10
|
+
export type ForEachPiecesPageOptions = {
|
|
11
|
+
api: IndexerApi
|
|
14
12
|
/** Source provider whose pieces are being repaired. */
|
|
15
13
|
providerId: bigint
|
|
16
14
|
/** Local repair row to attach operations to. */
|
|
17
15
|
repairId: number
|
|
18
16
|
/** Chain block number captured when the repair was created. */
|
|
19
17
|
blockNumber: bigint
|
|
20
|
-
/** Max indexer rows per page. Defaults to {@link DEFAULT_PIECES_PAGE_SIZE}. */
|
|
21
|
-
limit?: number
|
|
22
|
-
/** SQL offset for the indexer query. */
|
|
23
|
-
offset?: number
|
|
24
|
-
/**
|
|
25
|
-
* CIDs already emitted across prior pages. Pass the value returned from the previous call
|
|
26
|
-
* so the same CID is not queued twice when it appears in multiple source datasets.
|
|
27
|
-
*/
|
|
28
|
-
seenCids?: Set<string>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Result of a single {@link getPiecesPage} call. */
|
|
32
|
-
export type GetPiecesPageResult = {
|
|
33
|
-
/** `add_piece` operations ready to insert for this page (may include `skipped` rows). */
|
|
34
|
-
operations: OperationInsert[]
|
|
35
|
-
/** Whether another indexer page may exist after this one. */
|
|
36
|
-
hasMore: boolean
|
|
37
|
-
/** Offset to pass as `offset` on the next page. */
|
|
38
|
-
nextOffset: number
|
|
39
|
-
/** Updated dedupe set; pass into the next {@link getPiecesPage} call. */
|
|
40
|
-
seenCids: Set<string>
|
|
41
18
|
}
|
|
42
19
|
|
|
43
|
-
/** Options for
|
|
44
|
-
export type
|
|
45
|
-
|
|
46
|
-
/** Options for fetching one page of source-dataset pieces for replication. */
|
|
47
|
-
export type GetDataSetPiecesPageOptions = {
|
|
48
|
-
indexerDb: IndexerDatabase
|
|
20
|
+
/** Options for walking source-dataset pieces for replication. */
|
|
21
|
+
export type ForEachDataSetPiecesPageOptions = {
|
|
22
|
+
api: IndexerApi
|
|
49
23
|
/** Source dataset whose pieces are being replicated. */
|
|
50
24
|
dataSetId: bigint
|
|
51
25
|
/** Local job row to attach operations to. */
|
|
52
26
|
repairId: number
|
|
53
27
|
/** Provider URL that serves the source dataset pieces. */
|
|
54
28
|
sourceProviderUrl: string
|
|
55
|
-
/** Max indexer rows per page. Defaults to {@link DEFAULT_PIECES_PAGE_SIZE}. */
|
|
56
|
-
limit?: number
|
|
57
|
-
/** SQL offset for the indexer query. */
|
|
58
|
-
offset?: number
|
|
59
29
|
}
|
|
60
30
|
|
|
61
|
-
/**
|
|
62
|
-
export type
|
|
63
|
-
/** `add_piece` operations
|
|
31
|
+
/** One page of operations ready to insert into the local database. */
|
|
32
|
+
export type PiecesPage = {
|
|
33
|
+
/** `add_piece` operations for this page (repairs may include `skipped` rows). */
|
|
64
34
|
operations: OperationInsert[]
|
|
65
|
-
/** Whether another indexer page may exist after this one. */
|
|
66
|
-
hasMore: boolean
|
|
67
|
-
/** Offset to pass as `offset` on the next page. */
|
|
68
|
-
nextOffset: number
|
|
69
35
|
}
|
|
70
36
|
|
|
71
|
-
/** Options for {@link forEachDataSetPiecesPage}; pagination state is managed internally. */
|
|
72
|
-
export type ForEachDataSetPiecesPageOptions = Omit<GetDataSetPiecesPageOptions, 'offset'>
|
|
73
|
-
|
|
74
37
|
type PieceForOperation = {
|
|
75
38
|
cid: string
|
|
76
|
-
metadata: Record<string, string>
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Empty CID set for starting a paginated piece walk. */
|
|
80
|
-
export function emptySeenCids(): Set<string> {
|
|
81
|
-
return new Set()
|
|
39
|
+
metadata: Record<string, string>
|
|
82
40
|
}
|
|
83
41
|
|
|
84
42
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* Pieces are read from the indexer in stable dataset/piece order. CIDs are deduped globally so a
|
|
88
|
-
* piece stored under multiple source datasets is queued once into the single IPFS repair dataset.
|
|
89
|
-
* Alternate providers are resolved in one batch per page; operations without alternates are
|
|
90
|
-
* inserted as `skipped` with a descriptive error.
|
|
43
|
+
* Map pieces to repair `add_piece` operations, probing the alternate providers for a copy of each.
|
|
91
44
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* @param options - Indexer connection, repair context, and optional pagination state.
|
|
95
|
-
* @returns Operations for this page plus pagination cursors.
|
|
45
|
+
* Operations without a reachable alternate are returned as `skipped` with a descriptive error.
|
|
96
46
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
providerId,
|
|
47
|
+
async function piecesToRepairOperations({
|
|
48
|
+
pieces,
|
|
100
49
|
repairId,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const rows = await indexerDb
|
|
108
|
-
.select({
|
|
109
|
-
cid: schema.pieces.cid,
|
|
110
|
-
metadata: schema.pieces.metadata,
|
|
111
|
-
})
|
|
112
|
-
.from(schema.pieces)
|
|
113
|
-
.innerJoin(schema.dataSets, eq(schema.pieces.dataSetId, schema.dataSets.dataSetId))
|
|
114
|
-
.where(
|
|
115
|
-
and(
|
|
116
|
-
eq(schema.dataSets.providerId, providerId),
|
|
117
|
-
eq(schema.dataSets.deleted, false),
|
|
118
|
-
or(isNull(schema.dataSets.pdpEndEpoch), lte(schema.dataSets.pdpEndEpoch, blockNumber)),
|
|
119
|
-
eq(schema.pieces.removed, false)
|
|
120
|
-
)
|
|
121
|
-
)
|
|
122
|
-
.orderBy(asc(schema.pieces.dataSetId), asc(schema.pieces.pieceId))
|
|
123
|
-
.limit(limit)
|
|
124
|
-
.offset(offset)
|
|
125
|
-
|
|
50
|
+
alternateProviderUrls,
|
|
51
|
+
}: {
|
|
52
|
+
pieces: PieceForOperation[]
|
|
53
|
+
repairId: number
|
|
54
|
+
alternateProviderUrls: string[]
|
|
55
|
+
}): Promise<OperationInsert[]> {
|
|
126
56
|
const now = Date.now()
|
|
127
|
-
const pieces: PieceForOperation[] = []
|
|
128
|
-
|
|
129
|
-
for (const { cid, metadata } of rows) {
|
|
130
|
-
// Same CID can appear on multiple source datasets; only repair it once.
|
|
131
|
-
if (seenCids.has(cid)) continue
|
|
132
|
-
seenCids.add(cid)
|
|
133
|
-
|
|
134
|
-
pieces.push({ cid, metadata })
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Resolve pull sources in one query per page; exclude the provider being repaired from alternates.
|
|
138
|
-
const providersByCid = await findProvidersByCid({
|
|
139
|
-
indexerDb,
|
|
140
|
-
cids: pieces.map((piece) => piece.cid),
|
|
141
|
-
blockNumber,
|
|
142
|
-
})
|
|
143
57
|
|
|
144
|
-
|
|
58
|
+
return pMap(
|
|
145
59
|
pieces,
|
|
146
60
|
async ({ cid, metadata }) => {
|
|
147
|
-
const alternateProviders = providersByCid[cid]?.map((provider) => provider.serviceUrl) ?? []
|
|
148
61
|
let skippedMessage = ''
|
|
149
62
|
let validProvider: string | undefined
|
|
150
|
-
if (
|
|
151
|
-
validProvider = await Piece.findPieceOnProviders(
|
|
63
|
+
if (alternateProviderUrls.length > 0) {
|
|
64
|
+
validProvider = await Piece.findPieceOnProviders(alternateProviderUrls, Piece.from(cid))
|
|
152
65
|
|
|
153
66
|
if (!validProvider) {
|
|
154
|
-
skippedMessage = `
|
|
67
|
+
skippedMessage = `Piece not found on any of ${alternateProviderUrls.length} probed providers`
|
|
155
68
|
}
|
|
156
69
|
} else {
|
|
157
70
|
skippedMessage = `No alternate providers found`
|
|
@@ -163,129 +76,84 @@ export async function getPiecesPage({
|
|
|
163
76
|
// Cannot pull without another replica; mark skipped up front so run skips these ops.
|
|
164
77
|
status: validProvider ? 'pending' : 'skipped',
|
|
165
78
|
cid,
|
|
166
|
-
metadata
|
|
79
|
+
metadata,
|
|
167
80
|
alternateProvider: validProvider ?? '',
|
|
168
81
|
error: validProvider ? undefined : skippedMessage,
|
|
169
82
|
createdAt: now,
|
|
170
83
|
updatedAt: now,
|
|
171
|
-
}
|
|
84
|
+
} satisfies OperationInsert
|
|
172
85
|
},
|
|
173
86
|
{ concurrency: 20 }
|
|
174
87
|
)
|
|
175
|
-
|
|
176
|
-
// )
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
operations,
|
|
180
|
-
// A full page means there may be more rows; a short page ends pagination.
|
|
181
|
-
hasMore: rows.length === limit,
|
|
182
|
-
nextOffset: offset + rows.length,
|
|
183
|
-
seenCids,
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Fetch one page of pieces for a specific dataset and map them to replication operations.
|
|
189
|
-
*
|
|
190
|
-
* Unlike repairs, replication preserves source dataset ordering and does not dedupe repeated CIDs.
|
|
191
|
-
*
|
|
192
|
-
* @param options - Indexer connection, dataset context, and optional pagination state.
|
|
193
|
-
* @returns Operations for this page plus pagination cursors.
|
|
194
|
-
*/
|
|
195
|
-
export async function getDataSetPiecesPage({
|
|
196
|
-
indexerDb,
|
|
197
|
-
dataSetId,
|
|
198
|
-
repairId,
|
|
199
|
-
sourceProviderUrl,
|
|
200
|
-
limit = DEFAULT_PIECES_PAGE_SIZE,
|
|
201
|
-
offset = 0,
|
|
202
|
-
}: GetDataSetPiecesPageOptions): Promise<GetDataSetPiecesPageResult> {
|
|
203
|
-
const schema = indexerDb._.fullSchema
|
|
204
|
-
const rows = await indexerDb
|
|
205
|
-
.select({
|
|
206
|
-
cid: schema.pieces.cid,
|
|
207
|
-
metadata: schema.pieces.metadata,
|
|
208
|
-
})
|
|
209
|
-
.from(schema.pieces)
|
|
210
|
-
.where(and(eq(schema.pieces.dataSetId, dataSetId), eq(schema.pieces.removed, false)))
|
|
211
|
-
.orderBy(asc(schema.pieces.pieceId))
|
|
212
|
-
.limit(limit)
|
|
213
|
-
.offset(offset)
|
|
214
|
-
|
|
215
|
-
const now = Date.now()
|
|
216
|
-
const operations: OperationInsert[] = rows.map(({ cid, metadata }) => ({
|
|
217
|
-
repairId,
|
|
218
|
-
type: 'add_piece',
|
|
219
|
-
status: 'pending',
|
|
220
|
-
cid,
|
|
221
|
-
metadata: metadata ?? {},
|
|
222
|
-
alternateProvider: sourceProviderUrl,
|
|
223
|
-
createdAt: now,
|
|
224
|
-
updatedAt: now,
|
|
225
|
-
}))
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
operations,
|
|
229
|
-
hasMore: rows.length === limit,
|
|
230
|
-
nextOffset: offset + rows.length,
|
|
231
|
-
}
|
|
232
88
|
}
|
|
233
89
|
|
|
234
90
|
/**
|
|
235
|
-
* Walk every
|
|
91
|
+
* Walk every live piece of a provider at the repair block and map them to `add_piece` operations,
|
|
92
|
+
* invoking `onPage` per API page.
|
|
236
93
|
*
|
|
237
|
-
*
|
|
94
|
+
* Pieces are walked newest dataset and piece first. CIDs are deduped globally so a piece stored
|
|
95
|
+
* under multiple source datasets is queued once into the single IPFS repair dataset. Each piece is
|
|
96
|
+
* looked up directly on the approved or endorsed providers, then the provider being repaired
|
|
97
|
+
* (`HEAD /piece/{cid}`), rather than in the API, so finding a pull source costs no API requests.
|
|
238
98
|
*
|
|
239
|
-
* @param options -
|
|
240
|
-
* @param onPage - Async handler for each page
|
|
99
|
+
* @param options - API client and repair context.
|
|
100
|
+
* @param onPage - Async handler for each page (e.g. batch insert into local DB).
|
|
241
101
|
*/
|
|
242
102
|
export async function forEachPiecesPage(
|
|
243
|
-
|
|
244
|
-
onPage: (page:
|
|
103
|
+
{ api, providerId, repairId, blockNumber }: ForEachPiecesPageOptions,
|
|
104
|
+
onPage: (page: PiecesPage) => Promise<void>
|
|
245
105
|
): Promise<void> {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
hasMore = page.hasMore
|
|
106
|
+
const activeDataSetIds = await getActiveDataSetIds({ api, providerId, blockNumber })
|
|
107
|
+
const alternateProviderUrls = await getAlternateProviderUrls({ api, repairProviderId: providerId })
|
|
108
|
+
const seenCids = new Set<string>()
|
|
109
|
+
|
|
110
|
+
for await (const rows of providerPiecePages(api, providerId, { removed: false })) {
|
|
111
|
+
const pieces: PieceForOperation[] = []
|
|
112
|
+
for (const { cid, metadata, dataSetId } of rows) {
|
|
113
|
+
if (!activeDataSetIds.has(dataSetId)) continue
|
|
114
|
+
// Same CID can appear on multiple source datasets; only repair it once.
|
|
115
|
+
if (cid == null || seenCids.has(cid)) continue
|
|
116
|
+
seenCids.add(cid)
|
|
117
|
+
pieces.push({ cid, metadata: toStringMetadata(metadata) })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await onPage({ operations: await piecesToRepairOperations({ pieces, repairId, alternateProviderUrls }) })
|
|
262
121
|
}
|
|
263
122
|
}
|
|
264
123
|
|
|
265
124
|
/**
|
|
266
|
-
* Walk every
|
|
125
|
+
* Walk every live piece of a source dataset and map them to replication operations, invoking
|
|
126
|
+
* `onPage` per batch.
|
|
267
127
|
*
|
|
268
|
-
*
|
|
128
|
+
* Unlike repairs, replication preserves source dataset ordering (ascending piece ID) and does not
|
|
129
|
+
* dedupe repeated CIDs.
|
|
269
130
|
*
|
|
270
|
-
* @param options -
|
|
271
|
-
* @param onPage - Async handler for each page
|
|
131
|
+
* @param options - API client and dataset context.
|
|
132
|
+
* @param onPage - Async handler for each page (e.g. batch insert into local DB).
|
|
272
133
|
*/
|
|
273
134
|
export async function forEachDataSetPiecesPage(
|
|
274
|
-
|
|
275
|
-
onPage: (page:
|
|
135
|
+
{ api, dataSetId, repairId, sourceProviderUrl }: ForEachDataSetPiecesPageOptions,
|
|
136
|
+
onPage: (page: PiecesPage) => Promise<void>
|
|
276
137
|
): Promise<void> {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
while (hasMore) {
|
|
281
|
-
const page = await getDataSetPiecesPage({
|
|
282
|
-
...options,
|
|
283
|
-
offset,
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
await onPage(page)
|
|
138
|
+
// The API lists newest pieces first; load the whole dataset and reverse to keep source order.
|
|
139
|
+
const pieces = (await listDataSetPieces(api, dataSetId, { removed: false })).reverse()
|
|
140
|
+
const now = Date.now()
|
|
287
141
|
|
|
288
|
-
|
|
289
|
-
|
|
142
|
+
for (let offset = 0; offset < pieces.length; offset += MAX_PAGE_SIZE) {
|
|
143
|
+
const operations: OperationInsert[] = []
|
|
144
|
+
for (const { cid, metadata } of pieces.slice(offset, offset + MAX_PAGE_SIZE)) {
|
|
145
|
+
if (cid == null) continue
|
|
146
|
+
operations.push({
|
|
147
|
+
repairId,
|
|
148
|
+
type: 'add_piece',
|
|
149
|
+
status: 'pending',
|
|
150
|
+
cid,
|
|
151
|
+
metadata: toStringMetadata(metadata),
|
|
152
|
+
alternateProvider: sourceProviderUrl,
|
|
153
|
+
createdAt: now,
|
|
154
|
+
updatedAt: now,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
await onPage({ operations })
|
|
290
158
|
}
|
|
291
159
|
}
|