@filoz/repair-cli 1.0.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 (39) hide show
  1. package/dist/package.json +2 -2
  2. package/dist/src/commands/datasets.d.ts +2 -2
  3. package/dist/src/commands/datasets.js +9 -2
  4. package/dist/src/commands/datasets.js.map +1 -1
  5. package/dist/src/commands/providers.d.ts +2 -2
  6. package/dist/src/commands/repair.d.ts +2 -2
  7. package/dist/src/commands/repair.js +5 -1
  8. package/dist/src/commands/repair.js.map +1 -1
  9. package/dist/src/commands/replicate.d.ts +2 -2
  10. package/dist/src/commands/replicate.js +5 -1
  11. package/dist/src/commands/replicate.js.map +1 -1
  12. package/dist/src/commands/second-copy.d.ts +2 -2
  13. package/dist/src/commands/second-copy.js +5 -1
  14. package/dist/src/commands/second-copy.js.map +1 -1
  15. package/dist/src/commands/session-key.d.ts +2 -2
  16. package/dist/src/commands/wallet.d.ts +2 -2
  17. package/dist/src/middleware.d.ts +5 -5
  18. package/dist/src/middleware.d.ts.map +1 -1
  19. package/dist/src/middleware.js.map +1 -1
  20. package/dist/src/pipeline/add-pieces.d.ts +1 -1
  21. package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
  22. package/dist/src/pipeline/add-pieces.js +20 -15
  23. package/dist/src/pipeline/add-pieces.js.map +1 -1
  24. package/dist/src/types.d.ts +2 -2
  25. package/dist/src/types.d.ts.map +1 -1
  26. package/dist/src/utils.d.ts +38 -27
  27. package/dist/src/utils.d.ts.map +1 -1
  28. package/dist/src/utils.js +46 -0
  29. package/dist/src/utils.js.map +1 -1
  30. package/package.json +2 -2
  31. package/readme.md +6 -6
  32. package/src/commands/datasets.ts +9 -2
  33. package/src/commands/repair.ts +5 -1
  34. package/src/commands/replicate.ts +5 -1
  35. package/src/commands/second-copy.ts +5 -1
  36. package/src/middleware.ts +3 -3
  37. package/src/pipeline/add-pieces.ts +35 -17
  38. package/src/types.ts +2 -2
  39. package/src/utils.ts +69 -3
@@ -1,5 +1,4 @@
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'
@@ -10,14 +9,25 @@ import { repairUpdate } from '../db/repair-update.ts'
10
9
  import { upsertOperations } from '../db/upsert-operations.ts'
11
10
  import type { OperationSelect, RepairSelect } from '../local-schema.ts'
12
11
  import type { IndexerApi, LocalDatabase, WalletClient } from '../types.ts'
13
- import { completeConfirmedOperations, excludeOperationsByCid, hashLink, operationsToPullPieces } from '../utils.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
14
23
 
15
24
  export type RunPullPiecesPhaseOptions = {
16
25
  localDb: LocalDatabase
17
26
  api: IndexerApi
18
27
  repair: RepairSelect
19
28
  concurrency: number
20
- batchSize: number
29
+ /** Max pieces per batch. Defaults to as many as fit in one addPieces message. */
30
+ batchSize?: number
21
31
  client: WalletClient
22
32
  }
23
33
 
@@ -28,7 +38,6 @@ type CreateAddPiecesWorkerOptions = {
28
38
  repair: RepairSelect
29
39
  client: WalletClient
30
40
  state: {
31
- totalBatches: number
32
41
  totalOperations: number
33
42
  completedOperations: number
34
43
  failedOperations: number
@@ -50,7 +59,7 @@ function createAddPiecesWorker({ localDb, targetCids, repair, client, state, log
50
59
  let failedOps = 0
51
60
  let operations: OperationSelect[] = options.operations
52
61
 
53
- const group = log.group(`Batch ${options.batchNumber}/${state.totalBatches}`)
62
+ const group = log.group(`Batch ${options.batchNumber} (${options.operations.length} pieces)`)
54
63
 
55
64
  try {
56
65
  const dataset = await getTargetDataset({ localDb, repairId: repair.id, client })
@@ -76,7 +85,6 @@ function createAddPiecesWorker({ localDb, targetCids, repair, client, state, log
76
85
  dataSetId: dataset.dataSetId,
77
86
  clientDataSetId: dataset.clientDataSetId,
78
87
  pieces: operationsToPullPieces(operations),
79
- timeout: 1000 * 60 * 30, // 30 minutes
80
88
  onStatus: (status) => {
81
89
  const completed = status.pieces.filter((piece) => piece.status === 'complete').length
82
90
  const failed = status.pieces.filter((piece) => piece.status === 'failed').length
@@ -110,10 +118,7 @@ function createAddPiecesWorker({ localDb, targetCids, repair, client, state, log
110
118
  serviceURL: repair.targetProviderUrl,
111
119
  dataSetId: dataset.dataSetId,
112
120
  clientDataSetId: dataset.clientDataSetId,
113
- pieces: operations.map((operation) => ({
114
- pieceCid: Piece.from(operation.cid),
115
- metadata: targetCids ? undefined : operation.metadata,
116
- })),
121
+ pieces: operationsToAddPieces(operations, !targetCids),
117
122
  })
118
123
  await upsertOperations({
119
124
  localDb,
@@ -125,6 +130,8 @@ function createAddPiecesWorker({ localDb, targetCids, repair, client, state, log
125
130
 
126
131
  group.message(`Waiting for add pieces ${hashLink(addPiecesResult.txHash, client.chain)}...`)
127
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
128
135
  state.completedOperations += operations.length
129
136
  completedOps += operations.length
130
137
  for (const operation of operations) targetCids?.add(operation.cid)
@@ -134,7 +141,7 @@ function createAddPiecesWorker({ localDb, targetCids, repair, client, state, log
134
141
  ...operation,
135
142
  status: 'completed',
136
143
  error: null,
137
- txHash: waitForAddPieces.txHash,
144
+ txHash: confirmedTxHash,
138
145
  })),
139
146
  })
140
147
  }
@@ -179,7 +186,6 @@ export async function runAddPieces({
179
186
  )
180
187
  let batchNumber = 0
181
188
  const state = {
182
- totalBatches: Math.ceil(totalOperations / batchSize),
183
189
  totalOperations,
184
190
  completedOperations: 0,
185
191
  failedOperations: 0,
@@ -190,16 +196,20 @@ export async function runAddPieces({
190
196
  limit: 1,
191
197
  })
192
198
 
199
+ const dataset = totalOperations > 0 ? await getTargetDataset({ localDb, repairId: repair.id, client }) : undefined
200
+
193
201
  // Repairs and second copies dedupe into one shared dataset; read its CIDs once instead of per batch.
194
202
  let targetCids: Set<string> | undefined
195
- if (repair.repairDataSetId == null && totalOperations > 0) {
196
- const dataset = await getTargetDataset({ localDb, repairId: repair.id, client })
203
+ if (repair.repairDataSetId == null && dataset) {
197
204
  const pieces = await listDataSetPieces(api, dataset.dataSetId, { removed: false })
198
205
  targetCids = new Set(pieces.flatMap((piece) => (piece.cid == null ? [] : [piece.cid])))
199
206
  }
200
207
 
201
208
  async function getNextBatch(): Promise<OperationSelect[] | null> {
202
- const operations = await localDb.query.operations.findMany({
209
+ if (!dataset) {
210
+ return null
211
+ }
212
+ const candidates = await localDb.query.operations.findMany({
203
213
  where: and(
204
214
  eq(localSchema.operations.repairId, repair.id),
205
215
  eq(localSchema.operations.type, 'add_piece'),
@@ -207,12 +217,20 @@ export async function runAddPieces({
207
217
  gt(localSchema.operations.id, cursor)
208
218
  ),
209
219
  orderBy: [asc(localSchema.operations.id)],
210
- limit: batchSize,
220
+ limit: batchSize ?? MAX_BATCH_CANDIDATES,
211
221
  })
212
- if (operations.length === 0) {
222
+ if (candidates.length === 0) {
213
223
  return null
214
224
  }
215
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
+
216
234
  cursor = operations.at(-1)?.id ?? cursor
217
235
  return operations
218
236
  }
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ApiClient } from '@filecoin-project/api-client'
2
- import type { Chain } from '@filoz/synapse-core/chains'
2
+ import type { FilecoinChain } from '@filoz/synapse-core/chains'
3
3
  import type { Client } from '@libsql/client'
4
4
  import type { LibSQLDatabase } from 'drizzle-orm/libsql'
5
5
  import type { z } from 'incur'
@@ -26,7 +26,7 @@ export interface Config {
26
26
  dbPath: string
27
27
  }
28
28
 
29
- export type WalletClient = ViemClient<Transport, Chain, Account>
29
+ export type WalletClient = ViemClient<Transport, FilecoinChain, Account>
30
30
 
31
31
  export type Context = z.infer<typeof contextSchema>
32
32
 
package/src/utils.ts CHANGED
@@ -1,6 +1,6 @@
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
6
  import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
@@ -166,7 +166,7 @@ export async function migrateLocalDatabase(db: LocalDatabase) {
166
166
  * @param chain - The chain to use
167
167
  * @returns The link
168
168
  */
169
- export function hashLink(hash: string, chain: Chain) {
169
+ export function hashLink(hash: string, chain: FilecoinChain) {
170
170
  const link = terminalLink(hash, `${chain.blockExplorers?.default?.url}/tx/${hash}`)
171
171
  return link
172
172
  }
@@ -327,3 +327,69 @@ export function operationsToPullPieces(operations: OperationSelect[]) {
327
327
  }
328
328
  return pieces
329
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
+ }