@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
@@ -1,47 +1,27 @@
1
- import { and, eq } from 'drizzle-orm'
1
+ import type { Address } from 'viem'
2
+ import { getProvider } from '../api.ts'
2
3
  import type { Context, RepairProvider } from '../types.ts'
3
4
 
4
- export interface GetRepairProviderOptions extends Pick<Context, 'indexerDb'> {
5
+ export interface GetRepairProviderOptions extends Pick<Context, 'api'> {
5
6
  providerId: bigint
6
7
  }
7
8
 
8
9
  /**
9
10
  * Load an active provider by ID for use as a repair target.
10
11
  */
11
- export async function getRepairProvider({
12
- indexerDb,
13
- providerId,
14
- }: GetRepairProviderOptions): Promise<RepairProvider | null> {
15
- const schema = indexerDb._.fullSchema
16
- const [provider] = await indexerDb
17
- .select({
18
- providerId: schema.providers.providerId,
19
- providerAddress: schema.providers.providerAddress,
20
- name: schema.providers.name,
21
- serviceUrl: schema.providers.serviceUrl,
22
- approved: schema.providers.approved,
23
- endorsed: schema.providers.endorsed,
24
- })
25
- .from(schema.providers)
26
- .where(
27
- and(
28
- eq(schema.providers.providerId, providerId),
29
- eq(schema.providers.providerActive, true),
30
- eq(schema.providers.pdpProductActive, true)
31
- )
32
- )
33
- .limit(1)
12
+ export async function getRepairProvider({ api, providerId }: GetRepairProviderOptions): Promise<RepairProvider | null> {
13
+ const provider = await getProvider(api, providerId)
34
14
 
35
- if (!provider?.providerAddress || !provider?.serviceUrl || !provider?.name) {
15
+ if (!provider?.active || !provider.pdpProductActive || !provider.serviceUrl || !provider.name) {
36
16
  return null
37
17
  }
38
18
 
39
19
  return {
40
- providerId: provider.providerId,
41
- providerAddress: provider.providerAddress,
20
+ providerId: BigInt(provider.providerId),
21
+ providerAddress: provider.address as Address,
42
22
  name: provider.name,
43
23
  serviceUrl: provider.serviceUrl,
44
- approved: provider.approved,
45
- endorsed: provider.endorsed,
24
+ approved: provider.approved ?? false,
25
+ endorsed: provider.endorsed ?? false,
46
26
  }
47
27
  }
@@ -18,7 +18,7 @@ export interface RepairCreateOptions extends Context {
18
18
  * @returns {Promise<number>} The ID of the created repair.
19
19
  */
20
20
  export async function repairCreate(options: RepairCreateOptions): Promise<number> {
21
- const { indexerDb, localDb, repairProviderId, targetProviderId, client } = options
21
+ const { api, localDb, repairProviderId, targetProviderId, client } = options
22
22
  const localSchema = localDb._.fullSchema
23
23
  const now = Date.now()
24
24
  const blockNumber = await getBlockNumber(client)
@@ -34,7 +34,7 @@ export async function repairCreate(options: RepairCreateOptions): Promise<number
34
34
  throw new RepairCreationError('Target provider must differ from the provider being repaired')
35
35
  }
36
36
  const targetProvider = await getRepairProvider({
37
- indexerDb,
37
+ api,
38
38
  providerId: targetProviderId,
39
39
  })
40
40
 
@@ -68,7 +68,7 @@ export async function repairCreate(options: RepairCreateOptions): Promise<number
68
68
  const seenCids = new Set<string>()
69
69
  await forEachPiecesPage(
70
70
  {
71
- indexerDb,
71
+ api,
72
72
  providerId: repairProviderId,
73
73
  repairId: repair.id,
74
74
  blockNumber,
@@ -1,6 +1,6 @@
1
1
  import { taskLog } from '@clack/prompts'
2
- import { eq } from 'drizzle-orm'
3
2
  import { getBlockNumber } from 'viem/actions'
3
+ import { getDataSet } from '../api.ts'
4
4
  import { NoAlternateProviderError, RepairCreationError } from '../error.ts'
5
5
  import type { Context } from '../types.ts'
6
6
  import { forEachDataSetPiecesPage } from './get-pieces.ts'
@@ -18,8 +18,7 @@ export interface ReplicateCreateOptions extends Context {
18
18
  * @returns The ID of the created local job.
19
19
  */
20
20
  export async function replicateCreate(options: ReplicateCreateOptions): Promise<number> {
21
- const { indexerDb, localDb, dataSetId, targetProviderId, client } = options
22
- const indexerSchema = indexerDb._.fullSchema
21
+ const { api, localDb, dataSetId, targetProviderId, client } = options
23
22
  const localSchema = localDb._.fullSchema
24
23
  const now = Date.now()
25
24
  const blockNumber = await getBlockNumber(client)
@@ -30,35 +29,28 @@ export async function replicateCreate(options: ReplicateCreateOptions): Promise<
30
29
  retainLog: true,
31
30
  })
32
31
 
33
- const [sourceDataSet] = await indexerDb
34
- .select({
35
- dataSetId: indexerSchema.dataSets.dataSetId,
36
- providerId: indexerSchema.dataSets.providerId,
37
- deleted: indexerSchema.dataSets.deleted,
38
- })
39
- .from(indexerSchema.dataSets)
40
- .where(eq(indexerSchema.dataSets.dataSetId, dataSetId))
41
- .limit(1)
32
+ const sourceDataSet = await getDataSet(api, dataSetId)
42
33
 
43
- if (!sourceDataSet || sourceDataSet.deleted) {
34
+ if (!sourceDataSet || sourceDataSet.deleted || sourceDataSet.providerId == null) {
44
35
  throw new RepairCreationError(`Source dataset ${dataSetId} not found or deleted`)
45
36
  }
37
+ const sourceProviderId = BigInt(sourceDataSet.providerId)
46
38
 
47
- if (targetProviderId === sourceDataSet.providerId) {
39
+ if (targetProviderId === sourceProviderId) {
48
40
  throw new RepairCreationError('Target provider must differ from the source dataset provider')
49
41
  }
50
42
 
51
43
  const sourceProvider = await getRepairProvider({
52
- indexerDb,
53
- providerId: sourceDataSet.providerId,
44
+ api,
45
+ providerId: sourceProviderId,
54
46
  })
55
47
 
56
48
  if (!sourceProvider) {
57
- throw new RepairCreationError(`Source provider ${sourceDataSet.providerId} not found or inactive`)
49
+ throw new RepairCreationError(`Source provider ${sourceProviderId} not found or inactive`)
58
50
  }
59
51
 
60
52
  const targetProvider = await getRepairProvider({
61
- indexerDb,
53
+ api,
62
54
  providerId: targetProviderId,
63
55
  })
64
56
 
@@ -71,7 +63,7 @@ export async function replicateCreate(options: ReplicateCreateOptions): Promise<
71
63
  .values({
72
64
  kind: 'replicate',
73
65
  repairProviderId: sourceProvider.providerId,
74
- repairDataSetId: sourceDataSet.dataSetId,
66
+ repairDataSetId: dataSetId,
75
67
  sourceProviderUrl: sourceProvider.serviceUrl,
76
68
  targetProviderId: targetProvider.providerId,
77
69
  targetProviderUrl: targetProvider.serviceUrl,
@@ -87,8 +79,8 @@ export async function replicateCreate(options: ReplicateCreateOptions): Promise<
87
79
  let totalOperations = 0
88
80
  await forEachDataSetPiecesPage(
89
81
  {
90
- indexerDb,
91
- dataSetId: sourceDataSet.dataSetId,
82
+ api,
83
+ dataSetId,
92
84
  repairId: repair.id,
93
85
  sourceProviderUrl: sourceProvider.serviceUrl,
94
86
  },
@@ -76,7 +76,7 @@ export async function parsePieceCidsFromFile(piecesFile: string): Promise<string
76
76
  * @returns The ID of the created local job.
77
77
  */
78
78
  export async function secondCopyCreate(options: SecondCopyCreateOptions): Promise<number> {
79
- const { indexerDb, localDb, piecesFile, sourceProviderUrl, targetProviderId, client } = options
79
+ const { api, localDb, piecesFile, sourceProviderUrl, targetProviderId, client } = options
80
80
  const localSchema = localDb._.fullSchema
81
81
  const now = Date.now()
82
82
  const blockNumber = await getBlockNumber(client)
@@ -89,7 +89,7 @@ export async function secondCopyCreate(options: SecondCopyCreateOptions): Promis
89
89
  })
90
90
 
91
91
  const targetProvider = await getRepairProvider({
92
- indexerDb,
92
+ api,
93
93
  providerId: targetProviderId,
94
94
  })
95
95
 
package/src/middleware.ts CHANGED
@@ -1,25 +1,24 @@
1
- import type { Chain } from '@filoz/synapse-core/chains'
2
- import { drizzle as drizzlePostgres } from 'drizzle-orm/node-postgres'
1
+ import type { FilecoinChain } from '@filoz/synapse-core/chains'
3
2
  import { middleware, z } from 'incur'
4
3
  import type { Account, Client, Transport } from 'viem'
5
- import * as indexerSchema from './indexer-schema.ts'
6
- import type { IndexerDatabase, LocalDatabase } from './types.ts'
4
+ import { createIndexerApi } from './api.ts'
5
+ import type { IndexerApi, LocalDatabase } from './types.ts'
7
6
  import { config, createLocalDatabase, getClient } from './utils.ts'
8
7
 
9
8
  export const contextSchema = z.object({
10
- indexerDb: z.custom<IndexerDatabase>(),
9
+ api: z.custom<IndexerApi>(),
11
10
  localDb: z.custom<LocalDatabase>(),
12
11
  config: z.custom<typeof config>(),
13
- client: z.custom<Client<Transport, Chain, Account>>(),
14
- chain: z.custom<Chain>(),
12
+ client: z.custom<Client<Transport, FilecoinChain, Account>>(),
13
+ chain: z.custom<FilecoinChain>(),
15
14
  source: z.string(),
16
15
  isInteractive: z.boolean(),
17
16
  })
18
17
 
19
18
  export const contextMiddleware = middleware<typeof contextSchema>(async (c, next) => {
20
- const { dbPath, chainId, indexerMainnetUrl, indexerCalibrationUrl, source } = config.store
19
+ const { dbPath, chainId, apiUrl, source } = config.store
21
20
 
22
- if (!dbPath || !chainId || !indexerMainnetUrl || !indexerCalibrationUrl || !source) {
21
+ if (!dbPath || !chainId || !source) {
23
22
  return c.error({
24
23
  code: 'CONFIG_NOT_SET',
25
24
  message: 'Config not set. Please run `repair setup` first.',
@@ -28,13 +27,11 @@ export const contextMiddleware = middleware<typeof contextSchema>(async (c, next
28
27
  }
29
28
 
30
29
  const localDb = await createLocalDatabase(dbPath)
31
- const indexerDb = drizzlePostgres(chainId === 314 ? indexerMainnetUrl : indexerCalibrationUrl, {
32
- schema: indexerSchema,
33
- })
30
+ const api = createIndexerApi({ baseUrl: apiUrl, chainId })
34
31
 
35
32
  const { client, chain } = getClient(chainId)
36
33
  c.set('localDb', localDb)
37
- c.set('indexerDb', indexerDb)
34
+ c.set('api', api)
38
35
  c.set('config', config)
39
36
  c.set('client', client)
40
37
  c.set('chain', chain)
@@ -43,5 +40,4 @@ export const contextMiddleware = middleware<typeof contextSchema>(async (c, next
43
40
  await next()
44
41
 
45
42
  localDb.$client.close()
46
- await indexerDb.$client.end()
47
43
  })
@@ -1,32 +1,43 @@
1
1
  import { taskLog } from '@clack/prompts'
2
- import * as Piece from '@filoz/synapse-core/piece'
3
2
  import * as SP from '@filoz/synapse-core/sp'
4
3
  import { and, asc, eq, gt, inArray } from 'drizzle-orm'
5
4
  import PQueue from 'p-queue'
5
+ import { listDataSetPieces } from '../api.ts'
6
6
  import { dedupeCids } from '../db/dedupe-cids.ts'
7
7
  import { getTargetDataset } from '../db/get-target-dataset.ts'
8
8
  import { repairUpdate } from '../db/repair-update.ts'
9
9
  import { upsertOperations } from '../db/upsert-operations.ts'
10
10
  import type { OperationSelect, RepairSelect } from '../local-schema.ts'
11
- import type { IndexerDatabase, LocalDatabase, WalletClient } from '../types.ts'
12
- import { completeConfirmedOperations, excludeOperationsByCid, hashLink, operationsToPullPieces } from '../utils.ts'
11
+ import type { IndexerApi, LocalDatabase, WalletClient } from '../types.ts'
12
+ import {
13
+ completeConfirmedOperations,
14
+ excludeOperationsByCid,
15
+ fitAddPiecesBatch,
16
+ hashLink,
17
+ operationsToAddPieces,
18
+ operationsToPullPieces,
19
+ } from '../utils.ts'
20
+
21
+ /** Operations read per batch before fitting; above the ~400 metadata-free pieces one addPieces message holds. */
22
+ const MAX_BATCH_CANDIDATES = 500
13
23
 
14
24
  export type RunPullPiecesPhaseOptions = {
15
25
  localDb: LocalDatabase
16
- indexerDb: IndexerDatabase
26
+ api: IndexerApi
17
27
  repair: RepairSelect
18
28
  concurrency: number
19
- batchSize: number
29
+ /** Max pieces per batch. Defaults to as many as fit in one addPieces message. */
30
+ batchSize?: number
20
31
  client: WalletClient
21
32
  }
22
33
 
23
34
  type CreateAddPiecesWorkerOptions = {
24
35
  localDb: LocalDatabase
25
- indexerDb: IndexerDatabase
36
+ /** CIDs already in the target dataset; repairs only. Updated as batches are added. */
37
+ targetCids?: Set<string>
26
38
  repair: RepairSelect
27
39
  client: WalletClient
28
40
  state: {
29
- totalBatches: number
30
41
  totalOperations: number
31
42
  completedOperations: number
32
43
  failedOperations: number
@@ -42,21 +53,20 @@ type AddPiecesJobOptions = {
42
53
  /**
43
54
  * Create a worker function that adds pieces to the target repair dataset.
44
55
  */
45
- function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log }: CreateAddPiecesWorkerOptions) {
56
+ function createAddPiecesWorker({ localDb, targetCids, repair, client, state, log }: CreateAddPiecesWorkerOptions) {
46
57
  return async (options: AddPiecesJobOptions) => {
47
58
  let completedOps = 0
48
59
  let failedOps = 0
49
60
  let operations: OperationSelect[] = options.operations
50
- const isRepair = repair.repairDataSetId == null
51
61
 
52
- const group = log.group(`Batch ${options.batchNumber}/${state.totalBatches}`)
62
+ const group = log.group(`Batch ${options.batchNumber} (${options.operations.length} pieces)`)
53
63
 
54
64
  try {
55
65
  const dataset = await getTargetDataset({ localDb, repairId: repair.id, client })
56
66
 
57
67
  // dedupe operations by CID on the target dataset for repairs jobs
58
- if (isRepair) {
59
- operations = await dedupeCids({ indexerDb, localDb, dataSetId: dataset.dataSetId, operations })
68
+ if (targetCids) {
69
+ operations = await dedupeCids({ localDb, existingCids: targetCids, operations })
60
70
  }
61
71
  // A previous run may have submitted a transaction and crashed before marking rows completed.
62
72
  const operationsBeforeConfirmationCheck = operations.length
@@ -75,7 +85,6 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
75
85
  dataSetId: dataset.dataSetId,
76
86
  clientDataSetId: dataset.clientDataSetId,
77
87
  pieces: operationsToPullPieces(operations),
78
- timeout: 1000 * 60 * 30, // 30 minutes
79
88
  onStatus: (status) => {
80
89
  const completed = status.pieces.filter((piece) => piece.status === 'complete').length
81
90
  const failed = status.pieces.filter((piece) => piece.status === 'failed').length
@@ -109,10 +118,7 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
109
118
  serviceURL: repair.targetProviderUrl,
110
119
  dataSetId: dataset.dataSetId,
111
120
  clientDataSetId: dataset.clientDataSetId,
112
- pieces: operations.map((operation) => ({
113
- pieceCid: Piece.from(operation.cid),
114
- metadata: isRepair ? undefined : operation.metadata,
115
- })),
121
+ pieces: operationsToAddPieces(operations, !targetCids),
116
122
  })
117
123
  await upsertOperations({
118
124
  localDb,
@@ -124,15 +130,18 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
124
130
 
125
131
  group.message(`Waiting for add pieces ${hashLink(addPiecesResult.txHash, client.chain)}...`)
126
132
  const waitForAddPieces = await SP.waitForAddPieces(addPiecesResult)
133
+ // Curio may replace the message by fee; record the hash that landed on chain.
134
+ const confirmedTxHash = waitForAddPieces.confirmedTxHash ?? waitForAddPieces.txHash
127
135
  state.completedOperations += operations.length
128
136
  completedOps += operations.length
137
+ for (const operation of operations) targetCids?.add(operation.cid)
129
138
  await upsertOperations({
130
139
  localDb,
131
140
  operations: operations.map((operation) => ({
132
141
  ...operation,
133
142
  status: 'completed',
134
143
  error: null,
135
- txHash: waitForAddPieces.txHash,
144
+ txHash: confirmedTxHash,
136
145
  })),
137
146
  })
138
147
  }
@@ -158,7 +167,7 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
158
167
  */
159
168
  export async function runAddPieces({
160
169
  localDb,
161
- indexerDb,
170
+ api,
162
171
  repair,
163
172
  concurrency,
164
173
  batchSize,
@@ -177,7 +186,6 @@ export async function runAddPieces({
177
186
  )
178
187
  let batchNumber = 0
179
188
  const state = {
180
- totalBatches: Math.ceil(totalOperations / batchSize),
181
189
  totalOperations,
182
190
  completedOperations: 0,
183
191
  failedOperations: 0,
@@ -188,8 +196,20 @@ export async function runAddPieces({
188
196
  limit: 1,
189
197
  })
190
198
 
199
+ const dataset = totalOperations > 0 ? await getTargetDataset({ localDb, repairId: repair.id, client }) : undefined
200
+
201
+ // Repairs and second copies dedupe into one shared dataset; read its CIDs once instead of per batch.
202
+ let targetCids: Set<string> | undefined
203
+ if (repair.repairDataSetId == null && dataset) {
204
+ const pieces = await listDataSetPieces(api, dataset.dataSetId, { removed: false })
205
+ targetCids = new Set(pieces.flatMap((piece) => (piece.cid == null ? [] : [piece.cid])))
206
+ }
207
+
191
208
  async function getNextBatch(): Promise<OperationSelect[] | null> {
192
- const operations = await localDb.query.operations.findMany({
209
+ if (!dataset) {
210
+ return null
211
+ }
212
+ const candidates = await localDb.query.operations.findMany({
193
213
  where: and(
194
214
  eq(localSchema.operations.repairId, repair.id),
195
215
  eq(localSchema.operations.type, 'add_piece'),
@@ -197,17 +217,25 @@ export async function runAddPieces({
197
217
  gt(localSchema.operations.id, cursor)
198
218
  ),
199
219
  orderBy: [asc(localSchema.operations.id)],
200
- limit: batchSize,
220
+ limit: batchSize ?? MAX_BATCH_CANDIDATES,
201
221
  })
202
- if (operations.length === 0) {
222
+ if (candidates.length === 0) {
203
223
  return null
204
224
  }
205
225
 
226
+ // Size the batch by what fits in one addPieces message rather than a fixed piece count.
227
+ const operations = fitAddPiecesBatch({
228
+ operations: candidates,
229
+ dataSetId: dataset.dataSetId,
230
+ chain: client.chain,
231
+ withMetadata: !targetCids,
232
+ })
233
+
206
234
  cursor = operations.at(-1)?.id ?? cursor
207
235
  return operations
208
236
  }
209
237
 
210
- const addPiecesJob = createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log })
238
+ const addPiecesJob = createAddPiecesWorker({ localDb, targetCids, repair, client, state, log })
211
239
  const queue = new PQueue({ concurrency })
212
240
 
213
241
  while (true) {
@@ -1,18 +1,18 @@
1
1
  import * as p from '@clack/prompts'
2
2
  import * as SP from '@filoz/synapse-core/sp'
3
3
  import { getPDPProvider } from '@filoz/synapse-core/sp-registry'
4
- import { eq } from 'drizzle-orm'
5
4
  import type { Address } from 'viem'
5
+ import { getDataSet, toStringMetadata } from '../api.ts'
6
6
  import { findRepairDataset } from '../db/find-repair-dataset.ts'
7
7
  import { repairUpdate } from '../db/repair-update.ts'
8
8
  import type { RepairSelect } from '../local-schema.ts'
9
- import type { IndexerDatabase, LocalDatabase, WalletClient } from '../types.ts'
9
+ import type { IndexerApi, LocalDatabase, WalletClient } from '../types.ts'
10
10
  import { hashLink } from '../utils.ts'
11
11
 
12
12
  export type EnsureRepairDatasetOptions = {
13
13
  source: string
14
14
  localDb: LocalDatabase
15
- indexerDb: IndexerDatabase
15
+ api: IndexerApi
16
16
  client: WalletClient
17
17
  repair: RepairSelect
18
18
  payer: Address
@@ -23,14 +23,7 @@ export type EnsureRepairDatasetOptions = {
23
23
  *
24
24
  * @param options - The options for ensuring the repair dataset.
25
25
  */
26
- export async function ensureRepairDataset({
27
- source,
28
- localDb,
29
- indexerDb,
30
- client,
31
- repair,
32
- payer,
33
- }: EnsureRepairDatasetOptions) {
26
+ export async function ensureRepairDataset({ source, localDb, api, client, repair, payer }: EnsureRepairDatasetOptions) {
34
27
  const log = p.taskLog({
35
28
  title: 'Ensuring repair dataset',
36
29
  })
@@ -43,7 +36,7 @@ export async function ensureRepairDataset({
43
36
  let datasetId: bigint | null = null
44
37
  // check if dataset already exists
45
38
  const existingDatasetId = await findRepairDataset({
46
- indexerDb,
39
+ api,
47
40
  providerId: repair.targetProviderId,
48
41
  payer,
49
42
  source,
@@ -83,13 +76,7 @@ export async function ensureRepairDataset({
83
76
  *
84
77
  * @param options - The options for ensuring the replication dataset.
85
78
  */
86
- export async function ensureReplicateDataset({
87
- localDb,
88
- indexerDb,
89
- client,
90
- repair,
91
- payer,
92
- }: EnsureRepairDatasetOptions) {
79
+ export async function ensureReplicateDataset({ localDb, api, client, repair, payer }: EnsureRepairDatasetOptions) {
93
80
  const log = p.taskLog({
94
81
  title: 'Ensuring replication dataset',
95
82
  })
@@ -109,14 +96,7 @@ export async function ensureReplicateDataset({
109
96
  return repair.targetDataSetId
110
97
  }
111
98
 
112
- const schema = indexerDb._.fullSchema
113
- const sourceDataSet = await indexerDb.query.dataSets.findFirst({
114
- where: eq(schema.dataSets.dataSetId, repair.repairDataSetId),
115
- columns: {
116
- metadata: true,
117
- withCdn: true,
118
- },
119
- })
99
+ const sourceDataSet = await getDataSet(api, repair.repairDataSetId)
120
100
 
121
101
  if (!sourceDataSet) {
122
102
  throw new Error(`Source dataset ${repair.repairDataSetId} not found`)
@@ -126,8 +106,8 @@ export async function ensureReplicateDataset({
126
106
  payee: provider.payee,
127
107
  serviceURL: provider.pdp.serviceURL,
128
108
  payer,
129
- cdn: sourceDataSet.withCdn,
130
- metadata: sourceDataSet.metadata ?? undefined,
109
+ cdn: sourceDataSet.withCdn ?? false,
110
+ metadata: sourceDataSet.metadata ? toStringMetadata(sourceDataSet.metadata) : undefined,
131
111
  })
132
112
  log.message(`Waiting for data to be created at ${provider.pdp.serviceURL} ${hashLink(txHash, client.chain)}...`)
133
113
  const waitForResult = await SP.waitForCreateDataSet({
package/src/types.ts CHANGED
@@ -1,30 +1,32 @@
1
- import type { Chain } from '@filoz/synapse-core/chains'
1
+ import type { ApiClient } from '@filecoin-project/api-client'
2
+ import type { FilecoinChain } from '@filoz/synapse-core/chains'
2
3
  import type { Client } from '@libsql/client'
3
4
  import type { LibSQLDatabase } from 'drizzle-orm/libsql'
4
- import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
5
5
  import type { z } from 'incur'
6
- import type { Pool } from 'pg'
7
6
  import type { Account, Address, Hex, Transport, Client as ViemClient } from 'viem'
8
- import type * as indexerSchema from './indexer-schema.ts'
7
+ import type { Network } from './api.ts'
9
8
  import type * as localSchema from './local-schema.ts'
10
9
  import type { contextSchema } from './middleware.ts'
11
10
  export type LocalDatabase = LibSQLDatabase<typeof localSchema> & {
12
11
  $client: Client
13
12
  }
14
13
 
15
- export type IndexerDatabase = NodePgDatabase<typeof indexerSchema> & {
16
- $client: Pool
14
+ /**
15
+ * filecoin-api client bound to the configured network.
16
+ */
17
+ export type IndexerApi = {
18
+ client: ApiClient
19
+ network: Network
17
20
  }
18
21
 
19
22
  export interface Config {
20
23
  privateKey: Hex
21
- indexerMainnetUrl: string
22
- indexerCalibrationUrl: string
24
+ apiUrl: string
23
25
  chainId: number
24
26
  dbPath: string
25
27
  }
26
28
 
27
- export type WalletClient = ViemClient<Transport, Chain, Account>
29
+ export type WalletClient = ViemClient<Transport, FilecoinChain, Account>
28
30
 
29
31
  export type Context = z.infer<typeof contextSchema>
30
32
 
package/src/utils.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { type Chain, getChain } from '@filoz/synapse-core/chains'
1
+ import { type FilecoinChain, getChain } from '@filoz/synapse-core/chains'
2
2
  import * as Piece from '@filoz/synapse-core/piece'
3
- import type * as SP from '@filoz/synapse-core/sp'
3
+ import * as SP from '@filoz/synapse-core/sp'
4
4
  import { getTableColumns, inArray, type SQL, sql } from 'drizzle-orm'
5
5
  import { drizzle } from 'drizzle-orm/libsql'
6
- import type { PgTable } from 'drizzle-orm/pg-core'
7
6
  import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
8
7
  import { z } from 'incur'
9
8
  import { Conf } from 'iso-conf'
@@ -20,8 +19,7 @@ import type { LocalDatabase, WalletClient } from './types.ts'
20
19
 
21
20
  export const configSchema = z.object({
22
21
  privateKey: z.string().optional(),
23
- indexerMainnetUrl: z.url().optional(),
24
- indexerCalibrationUrl: z.url().optional(),
22
+ apiUrl: z.url().optional(),
25
23
  chainId: z.number().optional(),
26
24
  dbPath: z.string().optional(),
27
25
  source: z.string().optional(),
@@ -168,7 +166,7 @@ export async function migrateLocalDatabase(db: LocalDatabase) {
168
166
  * @param chain - The chain to use
169
167
  * @returns The link
170
168
  */
171
- export function hashLink(hash: string, chain: Chain) {
169
+ export function hashLink(hash: string, chain: FilecoinChain) {
172
170
  const link = terminalLink(hash, `${chain.blockExplorers?.default?.url}/tx/${hash}`)
173
171
  return link
174
172
  }
@@ -273,7 +271,7 @@ export async function findPieceOnProviders(providers: string[], pieceCid: string
273
271
  return result
274
272
  }
275
273
 
276
- export const buildConflictUpdateColumns = <T extends PgTable | SQLiteTable, Q extends keyof T['_']['columns']>(
274
+ export const buildConflictUpdateColumns = <T extends SQLiteTable, Q extends keyof T['_']['columns']>(
277
275
  table: T,
278
276
  columns?: Q[]
279
277
  ) => {
@@ -329,3 +327,69 @@ export function operationsToPullPieces(operations: OperationSelect[]) {
329
327
  }
330
328
  return pieces
331
329
  }
330
+
331
+ /**
332
+ * Convert operations to addPieces inputs. Repairs and second copies add pieces without metadata.
333
+ */
334
+ export function operationsToAddPieces(operations: OperationSelect[], withMetadata: boolean) {
335
+ return operations.map((operation) => ({
336
+ pieceCid: Piece.from(operation.cid),
337
+ metadata: withMetadata ? operation.metadata : undefined,
338
+ }))
339
+ }
340
+
341
+ /**
342
+ * Take the longest prefix of operations that fits in one addPieces message.
343
+ * Always returns at least one operation so an oversized or invalid piece fails in the worker instead of stalling the queue.
344
+ */
345
+ export function fitAddPiecesBatch({
346
+ operations,
347
+ dataSetId,
348
+ chain,
349
+ withMetadata,
350
+ }: {
351
+ operations: OperationSelect[]
352
+ dataSetId: bigint
353
+ chain: FilecoinChain
354
+ withMetadata: boolean
355
+ }): OperationSelect[] {
356
+ // Malformed CIDs throw; stop before the first one so it fails alone in the worker.
357
+ const pieces: SP.LimiterPiece[] = []
358
+ for (const operation of operations) {
359
+ try {
360
+ pieces.push(...operationsToAddPieces([operation], withMetadata))
361
+ } catch {
362
+ break
363
+ }
364
+ }
365
+ if (pieces.length === 0) {
366
+ return operations.slice(0, 1)
367
+ }
368
+
369
+ const fits = (count: number) => {
370
+ try {
371
+ return SP.addPiecesFits({
372
+ kind: 'addPieces',
373
+ dataSetId,
374
+ legacyPieceStorageIdLimit: chain.legacyPieceStorageIdLimit,
375
+ pieces: pieces.slice(0, count),
376
+ })
377
+ } catch {
378
+ // Invalid metadata throws; isolate that piece so addPieces fails it in the worker.
379
+ return false
380
+ }
381
+ }
382
+
383
+ // Calldata only grows as pieces are added, so binary search the largest prefix that fits.
384
+ let low = 1
385
+ let high = pieces.length
386
+ while (low < high) {
387
+ const mid = Math.ceil((low + high) / 2)
388
+ if (fits(mid)) {
389
+ low = mid
390
+ } else {
391
+ high = mid - 1
392
+ }
393
+ }
394
+ return operations.slice(0, low)
395
+ }
@@ -1,9 +0,0 @@
1
- import type { IndexerDatabase, RepairProvider } from '../types.ts';
2
- export type FindProvidersByCidOptions = {
3
- indexerDb: IndexerDatabase;
4
- cids: readonly string[];
5
- blockNumber: bigint;
6
- };
7
- export type ProvidersByCid = Record<string, RepairProvider[]>;
8
- export declare function findProvidersByCid({ indexerDb, cids, blockNumber, }: FindProvidersByCidOptions): Promise<ProvidersByCid>;
9
- //# sourceMappingURL=find-providers-by-cid.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"find-providers-by-cid.d.ts","sourceRoot":"","sources":["../../../src/db/find-providers-by-cid.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAElE,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,eAAe,CAAA;IAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAKD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CAAA;AAS7D,wBAAsB,kBAAkB,CAAC,EACvC,SAAS,EACT,IAAI,EACJ,WAAW,GACZ,EAAE,yBAAyB,GAAG,OAAO,CAAC,cAAc,CAAC,CA4CrD"}