@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.
Files changed (115) hide show
  1. package/dist/package.json +4 -4
  2. package/dist/src/api.d.ts +35 -0
  3. package/dist/src/api.d.ts.map +1 -0
  4. package/dist/src/api.js +118 -0
  5. package/dist/src/api.js.map +1 -0
  6. package/dist/src/commands/datasets.d.ts +5 -7
  7. package/dist/src/commands/datasets.d.ts.map +1 -1
  8. package/dist/src/commands/datasets.js +33 -35
  9. package/dist/src/commands/datasets.js.map +1 -1
  10. package/dist/src/commands/providers.d.ts +5 -7
  11. package/dist/src/commands/providers.d.ts.map +1 -1
  12. package/dist/src/commands/providers.js +32 -33
  13. package/dist/src/commands/providers.js.map +1 -1
  14. package/dist/src/commands/repair.d.ts +5 -7
  15. package/dist/src/commands/repair.d.ts.map +1 -1
  16. package/dist/src/commands/repair.js +5 -1
  17. package/dist/src/commands/repair.js.map +1 -1
  18. package/dist/src/commands/replicate.d.ts +5 -7
  19. package/dist/src/commands/replicate.d.ts.map +1 -1
  20. package/dist/src/commands/replicate.js +5 -1
  21. package/dist/src/commands/replicate.js.map +1 -1
  22. package/dist/src/commands/second-copy.d.ts +5 -7
  23. package/dist/src/commands/second-copy.d.ts.map +1 -1
  24. package/dist/src/commands/second-copy.js +5 -1
  25. package/dist/src/commands/second-copy.js.map +1 -1
  26. package/dist/src/commands/session-key.d.ts +5 -7
  27. package/dist/src/commands/session-key.d.ts.map +1 -1
  28. package/dist/src/commands/setup.d.ts.map +1 -1
  29. package/dist/src/commands/setup.js +11 -32
  30. package/dist/src/commands/setup.js.map +1 -1
  31. package/dist/src/commands/wallet.d.ts +5 -7
  32. package/dist/src/commands/wallet.d.ts.map +1 -1
  33. package/dist/src/db/dedupe-cids.d.ts +6 -7
  34. package/dist/src/db/dedupe-cids.d.ts.map +1 -1
  35. package/dist/src/db/dedupe-cids.js +2 -14
  36. package/dist/src/db/dedupe-cids.js.map +1 -1
  37. package/dist/src/db/find-repair-dataset.d.ts +3 -3
  38. package/dist/src/db/find-repair-dataset.d.ts.map +1 -1
  39. package/dist/src/db/find-repair-dataset.js +16 -14
  40. package/dist/src/db/find-repair-dataset.js.map +1 -1
  41. package/dist/src/db/get-active-data-set-ids.d.ts +9 -0
  42. package/dist/src/db/get-active-data-set-ids.d.ts.map +1 -0
  43. package/dist/src/db/get-active-data-set-ids.js +9 -0
  44. package/dist/src/db/get-active-data-set-ids.js.map +1 -0
  45. package/dist/src/db/get-alternate-provider-urls.d.ts +6 -0
  46. package/dist/src/db/get-alternate-provider-urls.d.ts.map +1 -0
  47. package/dist/src/db/get-alternate-provider-urls.js +24 -0
  48. package/dist/src/db/get-alternate-provider-urls.js.map +1 -0
  49. package/dist/src/db/get-pieces.d.ts +8 -27
  50. package/dist/src/db/get-pieces.d.ts.map +1 -1
  51. package/dist/src/db/get-pieces.js +44 -97
  52. package/dist/src/db/get-pieces.js.map +1 -1
  53. package/dist/src/db/get-repair-provider.d.ts +2 -2
  54. package/dist/src/db/get-repair-provider.d.ts.map +1 -1
  55. package/dist/src/db/get-repair-provider.js +8 -20
  56. package/dist/src/db/get-repair-provider.js.map +1 -1
  57. package/dist/src/db/repair-create.js +3 -3
  58. package/dist/src/db/repair-create.js.map +1 -1
  59. package/dist/src/db/replicate-create.d.ts.map +1 -1
  60. package/dist/src/db/replicate-create.js +13 -21
  61. package/dist/src/db/replicate-create.js.map +1 -1
  62. package/dist/src/db/second-copy-create.js +2 -2
  63. package/dist/src/db/second-copy-create.js.map +1 -1
  64. package/dist/src/local-schema.d.ts +1 -1
  65. package/dist/src/middleware.d.ts +12 -16
  66. package/dist/src/middleware.d.ts.map +1 -1
  67. package/dist/src/middleware.js +6 -10
  68. package/dist/src/middleware.js.map +1 -1
  69. package/dist/src/pipeline/add-pieces.d.ts +4 -4
  70. package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
  71. package/dist/src/pipeline/add-pieces.js +32 -19
  72. package/dist/src/pipeline/add-pieces.js.map +1 -1
  73. package/dist/src/pipeline/create-datasets.d.ts +4 -4
  74. package/dist/src/pipeline/create-datasets.d.ts.map +1 -1
  75. package/dist/src/pipeline/create-datasets.js +7 -14
  76. package/dist/src/pipeline/create-datasets.js.map +1 -1
  77. package/dist/src/types.d.ts +8 -9
  78. package/dist/src/types.d.ts.map +1 -1
  79. package/dist/src/utils.d.ts +45 -37
  80. package/dist/src/utils.d.ts.map +1 -1
  81. package/dist/src/utils.js +47 -2
  82. package/dist/src/utils.js.map +1 -1
  83. package/package.json +4 -4
  84. package/readme.md +22 -19
  85. package/src/api.ts +220 -0
  86. package/src/commands/datasets.ts +33 -38
  87. package/src/commands/providers.ts +34 -42
  88. package/src/commands/repair.ts +5 -1
  89. package/src/commands/replicate.ts +5 -1
  90. package/src/commands/second-copy.ts +5 -1
  91. package/src/commands/setup.ts +12 -35
  92. package/src/db/dedupe-cids.ts +7 -24
  93. package/src/db/find-repair-dataset.ts +17 -28
  94. package/src/db/get-active-data-set-ids.ts +29 -0
  95. package/src/db/get-alternate-provider-urls.ts +43 -0
  96. package/src/db/get-pieces.ts +82 -214
  97. package/src/db/get-repair-provider.ts +10 -30
  98. package/src/db/repair-create.ts +3 -3
  99. package/src/db/replicate-create.ts +13 -21
  100. package/src/db/second-copy-create.ts +2 -2
  101. package/src/middleware.ts +10 -14
  102. package/src/pipeline/add-pieces.ts +52 -24
  103. package/src/pipeline/create-datasets.ts +9 -29
  104. package/src/types.ts +11 -9
  105. package/src/utils.ts +71 -7
  106. package/dist/src/db/find-providers-by-cid.d.ts +0 -9
  107. package/dist/src/db/find-providers-by-cid.d.ts.map +0 -1
  108. package/dist/src/db/find-providers-by-cid.js +0 -43
  109. package/dist/src/db/find-providers-by-cid.js.map +0 -1
  110. package/dist/src/indexer-schema.d.ts +0 -549
  111. package/dist/src/indexer-schema.d.ts.map +0 -1
  112. package/dist/src/indexer-schema.js +0 -56
  113. package/dist/src/indexer-schema.js.map +0 -1
  114. package/src/db/find-providers-by-cid.ts +0 -70
  115. package/src/indexer-schema.ts +0 -77
@@ -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 validatePostgresUrl(value: string) {
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
- // Indexer URL
48
- const indexerMainnetUrl = await p.text({
49
- message: 'Enter your Mainnet Indexer Postgres URL',
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 || !validatePostgresUrl(value)) {
69
- return `Invalid postgres URL!`
46
+ if (!value || !validateHttpUrl(value)) {
47
+ return `Invalid http(s) URL!`
70
48
  }
71
49
  },
72
- initialValue: config.get('indexerCalibrationUrl'),
50
+ initialValue: config.get('apiUrl') ?? DEFAULT_API_URL,
73
51
  withGuide: false,
74
52
  })
75
- if (p.isCancel(indexerCalibrationUrl)) {
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('indexerMainnetUrl', indexerMainnetUrl)
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)
@@ -1,37 +1,20 @@
1
- import { and, eq, inArray } from 'drizzle-orm'
2
1
  import type { OperationSelect } from '../local-schema.ts'
3
- import type { IndexerDatabase, LocalDatabase } from '../types.ts'
2
+ import type { LocalDatabase } from '../types.ts'
4
3
  import { upsertOperations } from './upsert-operations.ts'
5
4
 
6
- export type SyncPiecesOnchainOptions = {
7
- indexerDb: IndexerDatabase
5
+ export type DedupeCidsOptions = {
8
6
  localDb: LocalDatabase
9
- dataSetId: bigint
7
+ /** CIDs already in the target repair dataset. */
8
+ existingCids: ReadonlySet<string>
10
9
  operations: OperationSelect[]
11
10
  }
12
11
 
13
12
  /**
14
- * Dedupe operations by CID on the target repair dataset.
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({ indexerDb, localDb, dataSetId, operations }: SyncPiecesOnchainOptions) {
18
- const cids = operations.map((operation) => operation.cid)
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 type { IndexerDatabase } from '../types.ts'
2
+ import { listDataSets } from '../api.ts'
3
+ import type { IndexerApi } from '../types.ts'
4
4
 
5
5
  export type FindRepairDatasetOptions = {
6
- indexerDb: IndexerDatabase
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 at the repair block, if it exists.
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
- indexerDb,
18
+ api,
19
19
  providerId,
20
20
  payer,
21
21
  source,
22
22
  }: FindRepairDatasetOptions): Promise<bigint | null> {
23
- const schema = indexerDb._.fullSchema
23
+ const dataSets = await listDataSets(api, {
24
+ owner: payer.toLowerCase(),
25
+ providerId,
26
+ deleted: false,
27
+ withCdn: false,
28
+ })
24
29
 
25
- const result = await indexerDb
26
- .select({
27
- dataSetId: schema.dataSets.dataSetId,
28
- })
29
- .from(schema.dataSets)
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 result[0].dataSetId
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
+ }
@@ -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 { IndexerDatabase } from '../types.ts'
6
- import { findProvidersByCid } from './find-providers-by-cid.ts'
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
- /** Default page size when paginating pieces from the indexer. */
9
- export const DEFAULT_PIECES_PAGE_SIZE = 3000
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 {@link forEachPiecesPage}; pagination state is managed internally. */
44
- export type ForEachPiecesPageOptions = Omit<GetPiecesPageOptions, 'offset' | 'seenCids'>
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
- /** Result of a single {@link getDataSetPiecesPage} call. */
62
- export type GetDataSetPiecesPageResult = {
63
- /** `add_piece` operations ready to insert for this page. */
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> | null
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
- * Fetch one page of pieces for a provider at the repair block and map them to `add_piece` operations.
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
- * Pass `seenCids` and `nextOffset` from the prior result to continue pagination.
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
- export async function getPiecesPage({
98
- indexerDb,
99
- providerId,
47
+ async function piecesToRepairOperations({
48
+ pieces,
100
49
  repairId,
101
- blockNumber,
102
- limit = DEFAULT_PIECES_PAGE_SIZE,
103
- offset = 0,
104
- seenCids = emptySeenCids(),
105
- }: GetPiecesPageOptions): Promise<GetPiecesPageResult> {
106
- const schema = indexerDb._.fullSchema
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
- const operations: OperationInsert[] = await pMap(
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 (alternateProviders.length > 0) {
151
- validProvider = await Piece.findPieceOnProviders(alternateProviders, Piece.from(cid))
63
+ if (alternateProviderUrls.length > 0) {
64
+ validProvider = await Piece.findPieceOnProviders(alternateProviderUrls, Piece.from(cid))
152
65
 
153
66
  if (!validProvider) {
154
- skippedMessage = `Found ${alternateProviders.length} alternate providers, but none are valid. ${alternateProviders.join(', ')}`
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: 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 page of `add_piece` operations for a provider, invoking `onPage` per batch.
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
- * Manages `offset` and `seenCids` across pages so callers only handle inserts.
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 - Same inputs as {@link getPiecesPage} except pagination cursors.
240
- * @param onPage - Async handler for each page result (e.g. batch insert into local DB).
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
- options: ForEachPiecesPageOptions,
244
- onPage: (page: GetPiecesPageResult) => Promise<void>
103
+ { api, providerId, repairId, blockNumber }: ForEachPiecesPageOptions,
104
+ onPage: (page: PiecesPage) => Promise<void>
245
105
  ): Promise<void> {
246
- let offset = 0
247
- let hasMore = true
248
- let seenCids = emptySeenCids()
249
-
250
- while (hasMore) {
251
- const page = await getPiecesPage({
252
- ...options,
253
- offset,
254
- seenCids,
255
- })
256
-
257
- await onPage(page)
258
-
259
- offset = page.nextOffset
260
- seenCids = page.seenCids
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 page of `add_piece` operations for a source dataset, invoking `onPage` per batch.
125
+ * Walk every live piece of a source dataset and map them to replication operations, invoking
126
+ * `onPage` per batch.
267
127
  *
268
- * Replication pagination intentionally has no CID dedupe state so duplicate pieces are preserved.
128
+ * Unlike repairs, replication preserves source dataset ordering (ascending piece ID) and does not
129
+ * dedupe repeated CIDs.
269
130
  *
270
- * @param options - Same inputs as {@link getDataSetPiecesPage} except pagination cursor.
271
- * @param onPage - Async handler for each page result (e.g. batch insert into local DB).
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
- options: ForEachDataSetPiecesPageOptions,
275
- onPage: (page: GetDataSetPiecesPageResult) => Promise<void>
135
+ { api, dataSetId, repairId, sourceProviderUrl }: ForEachDataSetPiecesPageOptions,
136
+ onPage: (page: PiecesPage) => Promise<void>
276
137
  ): Promise<void> {
277
- let offset = 0
278
- let hasMore = true
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
- offset = page.nextOffset
289
- hasMore = page.hasMore
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
  }