@filoz/repair-cli 0.0.1

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 (127) hide show
  1. package/dist/src/cli.d.ts +3 -0
  2. package/dist/src/cli.d.ts.map +1 -0
  3. package/dist/src/cli.js +19 -0
  4. package/dist/src/cli.js.map +1 -0
  5. package/dist/src/commands/datasets.d.ts +9 -0
  6. package/dist/src/commands/datasets.d.ts.map +1 -0
  7. package/dist/src/commands/datasets.js +59 -0
  8. package/dist/src/commands/datasets.js.map +1 -0
  9. package/dist/src/commands/providers.d.ts +9 -0
  10. package/dist/src/commands/providers.d.ts.map +1 -0
  11. package/dist/src/commands/providers.js +84 -0
  12. package/dist/src/commands/providers.js.map +1 -0
  13. package/dist/src/commands/repair.d.ts +9 -0
  14. package/dist/src/commands/repair.d.ts.map +1 -0
  15. package/dist/src/commands/repair.js +170 -0
  16. package/dist/src/commands/repair.js.map +1 -0
  17. package/dist/src/commands/setup.d.ts +11 -0
  18. package/dist/src/commands/setup.d.ts.map +1 -0
  19. package/dist/src/commands/setup.js +127 -0
  20. package/dist/src/commands/setup.js.map +1 -0
  21. package/dist/src/commands/wallet.d.ts +9 -0
  22. package/dist/src/commands/wallet.d.ts.map +1 -0
  23. package/dist/src/commands/wallet.js +150 -0
  24. package/dist/src/commands/wallet.js.map +1 -0
  25. package/dist/src/db/get-pieces.d.ts +23 -0
  26. package/dist/src/db/get-pieces.d.ts.map +1 -0
  27. package/dist/src/db/get-pieces.js +84 -0
  28. package/dist/src/db/get-pieces.js.map +1 -0
  29. package/dist/src/db/get-providers-by-cid.d.ts +10 -0
  30. package/dist/src/db/get-providers-by-cid.d.ts.map +1 -0
  31. package/dist/src/db/get-providers-by-cid.js +45 -0
  32. package/dist/src/db/get-providers-by-cid.js.map +1 -0
  33. package/dist/src/db/get-repair-dataset.d.ts +9 -0
  34. package/dist/src/db/get-repair-dataset.d.ts.map +1 -0
  35. package/dist/src/db/get-repair-dataset.js +15 -0
  36. package/dist/src/db/get-repair-dataset.js.map +1 -0
  37. package/dist/src/db/get-repair-provider.d.ts +6 -0
  38. package/dist/src/db/get-repair-provider.d.ts.map +1 -0
  39. package/dist/src/db/get-repair-provider.js +28 -0
  40. package/dist/src/db/get-repair-provider.js.map +1 -0
  41. package/dist/src/db/get-target-dataset.d.ts +7 -0
  42. package/dist/src/db/get-target-dataset.d.ts.map +1 -0
  43. package/dist/src/db/get-target-dataset.js +27 -0
  44. package/dist/src/db/get-target-dataset.js.map +1 -0
  45. package/dist/src/db/repair-create.d.ts +7 -0
  46. package/dist/src/db/repair-create.d.ts.map +1 -0
  47. package/dist/src/db/repair-create.js +69 -0
  48. package/dist/src/db/repair-create.js.map +1 -0
  49. package/dist/src/db/repair-delete.d.ts +11 -0
  50. package/dist/src/db/repair-delete.d.ts.map +1 -0
  51. package/dist/src/db/repair-delete.js +22 -0
  52. package/dist/src/db/repair-delete.js.map +1 -0
  53. package/dist/src/db/repair-update.d.ts +10 -0
  54. package/dist/src/db/repair-update.d.ts.map +1 -0
  55. package/dist/src/db/repair-update.js +13 -0
  56. package/dist/src/db/repair-update.js.map +1 -0
  57. package/dist/src/db/sync-pieces-onchain.d.ts +10 -0
  58. package/dist/src/db/sync-pieces-onchain.d.ts.map +1 -0
  59. package/dist/src/db/sync-pieces-onchain.js +35 -0
  60. package/dist/src/db/sync-pieces-onchain.js.map +1 -0
  61. package/dist/src/db/update-operation.d.ts +11 -0
  62. package/dist/src/db/update-operation.d.ts.map +1 -0
  63. package/dist/src/db/update-operation.js +14 -0
  64. package/dist/src/db/update-operation.js.map +1 -0
  65. package/dist/src/db/upsert-operations.d.ts +8 -0
  66. package/dist/src/db/upsert-operations.d.ts.map +1 -0
  67. package/dist/src/db/upsert-operations.js +13 -0
  68. package/dist/src/db/upsert-operations.js.map +1 -0
  69. package/dist/src/error.d.ts +14 -0
  70. package/dist/src/error.d.ts.map +1 -0
  71. package/dist/src/error.js +27 -0
  72. package/dist/src/error.js.map +1 -0
  73. package/dist/src/indexer-schema.d.ts +549 -0
  74. package/dist/src/indexer-schema.d.ts.map +1 -0
  75. package/dist/src/indexer-schema.js +56 -0
  76. package/dist/src/indexer-schema.js.map +1 -0
  77. package/dist/src/local-schema.d.ts +456 -0
  78. package/dist/src/local-schema.d.ts.map +1 -0
  79. package/dist/src/local-schema.js +63 -0
  80. package/dist/src/local-schema.js.map +1 -0
  81. package/dist/src/middleware.d.ts +19 -0
  82. package/dist/src/middleware.d.ts.map +1 -0
  83. package/dist/src/middleware.js +28 -0
  84. package/dist/src/middleware.js.map +1 -0
  85. package/dist/src/pipeline/create-datasets.d.ts +10 -0
  86. package/dist/src/pipeline/create-datasets.d.ts.map +1 -0
  87. package/dist/src/pipeline/create-datasets.js +48 -0
  88. package/dist/src/pipeline/create-datasets.js.map +1 -0
  89. package/dist/src/pipeline/pull.d.ts +30 -0
  90. package/dist/src/pipeline/pull.d.ts.map +1 -0
  91. package/dist/src/pipeline/pull.js +169 -0
  92. package/dist/src/pipeline/pull.js.map +1 -0
  93. package/dist/src/types.d.ts +34 -0
  94. package/dist/src/types.d.ts.map +1 -0
  95. package/dist/src/types.js +1 -0
  96. package/dist/src/types.js.map +1 -0
  97. package/dist/src/utils.d.ts +11945 -0
  98. package/dist/src/utils.d.ts.map +1 -0
  99. package/dist/src/utils.js +121 -0
  100. package/dist/src/utils.js.map +1 -0
  101. package/package.json +135 -0
  102. package/readme.md +250 -0
  103. package/src/cli.ts +20 -0
  104. package/src/commands/datasets.ts +62 -0
  105. package/src/commands/providers.ts +99 -0
  106. package/src/commands/repair.ts +177 -0
  107. package/src/commands/setup.ts +142 -0
  108. package/src/commands/wallet.ts +159 -0
  109. package/src/db/get-pieces.ts +189 -0
  110. package/src/db/get-providers-by-cid.ts +75 -0
  111. package/src/db/get-repair-dataset.ts +44 -0
  112. package/src/db/get-repair-provider.ts +47 -0
  113. package/src/db/get-target-dataset.ts +47 -0
  114. package/src/db/repair-create.ts +101 -0
  115. package/src/db/repair-delete.ts +39 -0
  116. package/src/db/repair-update.ts +20 -0
  117. package/src/db/sync-pieces-onchain.ts +53 -0
  118. package/src/db/update-operation.ts +26 -0
  119. package/src/db/upsert-operations.ts +23 -0
  120. package/src/error.ts +33 -0
  121. package/src/indexer-schema.ts +77 -0
  122. package/src/local-schema.ts +91 -0
  123. package/src/middleware.ts +34 -0
  124. package/src/pipeline/create-datasets.ts +70 -0
  125. package/src/pipeline/pull.ts +255 -0
  126. package/src/types.ts +41 -0
  127. package/src/utils.ts +190 -0
package/src/error.ts ADDED
@@ -0,0 +1,33 @@
1
+ export class NoAlternateProviderError extends Error {
2
+ readonly providerId?: bigint
3
+
4
+ /**
5
+ * @param providerId - When set, the explicit target provider was not found or inactive.
6
+ */
7
+ constructor(providerId?: bigint) {
8
+ super(providerId == null ? 'No alternate provider found' : `Target provider ${providerId} not found or inactive`)
9
+ this.name = 'NoAlternateProviderError'
10
+ this.providerId = providerId
11
+ }
12
+ }
13
+
14
+ export class RepairCreationError extends Error {
15
+ constructor(message = 'Failed to create repair row') {
16
+ super(message)
17
+ this.name = 'RepairCreationError'
18
+ }
19
+ }
20
+
21
+ export class RepairNotFoundError extends Error {
22
+ constructor(repairId: number) {
23
+ super(`Repair ${repairId} not found`)
24
+ this.name = 'RepairNotFoundError'
25
+ }
26
+ }
27
+
28
+ export class MissingRepairDataSetError extends Error {
29
+ constructor() {
30
+ super('Missing repair dataset ID')
31
+ this.name = 'MissingRepairDataSetError'
32
+ }
33
+ }
@@ -0,0 +1,77 @@
1
+ import { relations } from 'drizzle-orm'
2
+ import { bigint, boolean, index, jsonb, pgSchema, primaryKey, text } from 'drizzle-orm/pg-core'
3
+ import type { Address } from 'viem'
4
+
5
+ export type JsonRecord = Record<string, string>
6
+
7
+ const schema = pgSchema('early-repair')
8
+
9
+ export const providers = schema.table(
10
+ 'providers',
11
+ {
12
+ providerId: bigint('provider_id', { mode: 'bigint' }).primaryKey(),
13
+ providerAddress: text('provider_address').$type<Address>(),
14
+ name: text('name'),
15
+ serviceUrl: text('service_url'),
16
+ providerActive: boolean('provider_active').notNull(),
17
+ pdpProductActive: boolean('pdp_product_active').notNull(),
18
+ approved: boolean('approved').notNull().default(false),
19
+ endorsed: boolean('endorsed').notNull().default(false),
20
+ createdAtBlock: bigint('created_at_block', { mode: 'bigint' }),
21
+ updatedAtBlock: bigint('updated_at_block', { mode: 'bigint' }).notNull(),
22
+ },
23
+ (table) => [index('providers_provider_address_idx').on(table.providerAddress)]
24
+ )
25
+
26
+ export const providersRelations = relations(providers, ({ many }) => ({
27
+ dataSets: many(dataSets),
28
+ }))
29
+
30
+ export const dataSets = schema.table(
31
+ 'data_sets',
32
+ {
33
+ dataSetId: bigint('data_set_id', { mode: 'bigint' }).primaryKey(),
34
+ providerId: bigint('provider_id', { mode: 'bigint' }).notNull(),
35
+ metadata: jsonb('metadata').$type<JsonRecord | null>(),
36
+ payer: text('payer').notNull(),
37
+ source: text('source'),
38
+ withCdn: boolean('with_cdn').notNull(),
39
+ withIpfsIndexing: boolean('with_ipfs_indexing').notNull(),
40
+ pdpEndEpoch: bigint('pdp_end_epoch', { mode: 'bigint' }).notNull(),
41
+ deleted: boolean('deleted').notNull(),
42
+ createdAtBlock: bigint('created_at_block', { mode: 'bigint' }).notNull(),
43
+ updatedAtBlock: bigint('updated_at_block', { mode: 'bigint' }).notNull(),
44
+ },
45
+ (table) => [index('data_sets_provider_id_idx').on(table.providerId)]
46
+ )
47
+
48
+ export const dataSetsRelations = relations(dataSets, ({ one, many }) => ({
49
+ provider: one(providers, {
50
+ fields: [dataSets.providerId],
51
+ references: [providers.providerId],
52
+ }),
53
+ pieces: many(pieces),
54
+ }))
55
+
56
+ export const pieces = schema.table(
57
+ 'pieces',
58
+ {
59
+ dataSetId: bigint('data_set_id', { mode: 'bigint' }).notNull(),
60
+ pieceId: bigint('piece_id', { mode: 'bigint' }).notNull(),
61
+ cid: text('cid').notNull(),
62
+ rawSize: bigint('raw_size', { mode: 'bigint' }).notNull(),
63
+ metadata: jsonb('metadata').$type<JsonRecord | null>(),
64
+ removed: boolean('removed').notNull(),
65
+ addedAtBlock: bigint('added_at_block', { mode: 'bigint' }).notNull(),
66
+ removedAtBlock: bigint('removed_at_block', { mode: 'bigint' }),
67
+ updatedAtBlock: bigint('updated_at_block', { mode: 'bigint' }).notNull(),
68
+ },
69
+ (table) => [primaryKey({ columns: [table.dataSetId, table.pieceId] }), index('pieces_cid_idx').on(table.cid)]
70
+ )
71
+
72
+ export const piecesRelations = relations(pieces, ({ one }) => ({
73
+ dataSet: one(dataSets, {
74
+ fields: [pieces.dataSetId],
75
+ references: [dataSets.dataSetId],
76
+ }),
77
+ }))
@@ -0,0 +1,91 @@
1
+ import type { MetadataObject } from '@filoz/synapse-core'
2
+ import type * as SP from '@filoz/synapse-core/sp'
3
+ import { relations } from 'drizzle-orm'
4
+ import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core'
5
+ import * as t from 'drizzle-orm/sqlite-core'
6
+ import { customType, sqliteTable as table } from 'drizzle-orm/sqlite-core'
7
+ import * as Json from 'iso-base/json'
8
+
9
+ export type RepairStatus = 'pending' | 'completed' | 'failed'
10
+ export type OperationStatus = 'pending' | 'completed' | 'failed' | 'skipped'
11
+ export type OperationType = 'create_dataset' | 'add_piece'
12
+
13
+ export type OperationResult = Omit<
14
+ SP.AddPiecesSuccess,
15
+ 'txStatus' | 'addMessageOk' | 'piecesAdded' | 'pieceCount' | 'confirmedPieceIds'
16
+ >
17
+
18
+ /**
19
+ * Custom type for JSON
20
+ * It will be used to store JSON data in the database
21
+ */
22
+ export const jsonType = customType<{ data: unknown }>({
23
+ dataType() {
24
+ return 'text'
25
+ },
26
+ toDriver(value) {
27
+ return Json.stringify(value)
28
+ },
29
+ fromDriver(value) {
30
+ return Json.parse(value as string)
31
+ },
32
+ })
33
+
34
+ export const bigintType = customType<{ data: bigint }>({
35
+ dataType() {
36
+ return 'text'
37
+ },
38
+ toDriver(value) {
39
+ return value.toString()
40
+ },
41
+ fromDriver(value) {
42
+ return BigInt(value as string)
43
+ },
44
+ })
45
+
46
+ export type RepairInsert = typeof repairs.$inferInsert
47
+ export type RepairSelect = typeof repairs.$inferSelect
48
+ export type RepairUpdate = Partial<RepairInsert>
49
+
50
+ export const repairs = table('repairs', {
51
+ id: t.int().primaryKey({ autoIncrement: true }),
52
+ status: t.text().$type<RepairStatus>().notNull().default('pending'),
53
+ repairProviderId: bigintType('repair_provider_id').notNull(),
54
+ targetProviderId: bigintType('target_provider_id').notNull(),
55
+ targetProviderUrl: t.text('target_provider_url').notNull(),
56
+ targetDataSetId: bigintType('target_data_set_id'),
57
+ blockNumber: bigintType('block_number').notNull(),
58
+ createdAt: t.integer('created_at').notNull(),
59
+ updatedAt: t.integer('updated_at').notNull(),
60
+ })
61
+
62
+ export type OperationInsert = typeof operations.$inferInsert
63
+ export type OperationSelect = typeof operations.$inferSelect
64
+
65
+ export const operations = table('operations', {
66
+ id: t.int().primaryKey({ autoIncrement: true }),
67
+ repairId: t
68
+ .int('repair_id')
69
+ .references((): AnySQLiteColumn => repairs.id)
70
+ .notNull(),
71
+ type: t.text().$type<OperationType>().notNull(),
72
+ status: t.text().$type<OperationStatus>().notNull().default('pending'),
73
+ cid: t.text().notNull(),
74
+ metadata: jsonType().$type<MetadataObject>().notNull(),
75
+ alternateProvider: t.text('alternate_provider').notNull(),
76
+ result: jsonType().$type<OperationResult>(),
77
+ error: t.text(),
78
+ createdAt: t.integer('created_at').notNull(),
79
+ updatedAt: t.integer('updated_at').notNull(),
80
+ })
81
+
82
+ export const repairRelations = relations(repairs, ({ many }) => ({
83
+ operations: many(operations),
84
+ }))
85
+
86
+ export const operationRelations = relations(operations, ({ one }) => ({
87
+ repair: one(repairs, {
88
+ fields: [operations.repairId],
89
+ references: [repairs.id],
90
+ }),
91
+ }))
@@ -0,0 +1,34 @@
1
+ import type { Chain } from '@filoz/synapse-core/chains'
2
+ import { drizzle as drizzlePostgres } from 'drizzle-orm/node-postgres'
3
+ import { middleware, z } from 'incur'
4
+ import type { Account, Client, Transport } from 'viem'
5
+ import * as indexerSchema from './indexer-schema.ts'
6
+ import type { IndexerDatabase, LocalDatabase } from './types.ts'
7
+ import { config, createLocalDatabase, getClient } from './utils.ts'
8
+
9
+ export const contextSchema = z.object({
10
+ indexerDb: z.custom<IndexerDatabase>(),
11
+ localDb: z.custom<LocalDatabase>(),
12
+ config: z.custom<typeof config>(),
13
+ client: z.custom<Client<Transport, Chain, Account>>(),
14
+ chain: z.custom<Chain>(),
15
+ })
16
+
17
+ export const contextMiddleware = middleware<typeof contextSchema>(async (c, next) => {
18
+ const { dbPath, chainId, indexerMainnetUrl, indexerCalibrationUrl } = config.store
19
+ const localDb = await createLocalDatabase(dbPath)
20
+ const indexerDb = drizzlePostgres(chainId === 314 ? indexerMainnetUrl : indexerCalibrationUrl, {
21
+ schema: indexerSchema,
22
+ })
23
+
24
+ const { client, chain } = getClient(chainId)
25
+ c.set('localDb', localDb)
26
+ c.set('indexerDb', indexerDb)
27
+ c.set('config', config)
28
+ c.set('client', client)
29
+ c.set('chain', chain)
30
+ await next()
31
+
32
+ localDb.$client.close()
33
+ await indexerDb.$client.end()
34
+ })
@@ -0,0 +1,70 @@
1
+ import * as p from '@clack/prompts'
2
+ import * as SP from '@filoz/synapse-core/sp'
3
+ import { getPDPProvider } from '@filoz/synapse-core/sp-registry'
4
+ import { getRepairDataset } from '../db/get-repair-dataset.ts'
5
+ import { repairUpdate } from '../db/repair-update.ts'
6
+ import type { RepairSelect } from '../local-schema.ts'
7
+ import type { IndexerDatabase, LocalDatabase, WalletClient } from '../types.ts'
8
+ import { getRepairDatasetMetadata, hashLink } from '../utils.ts'
9
+
10
+ export type EnsureRepairDatasetOptions = {
11
+ localDb: LocalDatabase
12
+ indexerDb: IndexerDatabase
13
+ client: WalletClient
14
+ repair: RepairSelect
15
+ }
16
+
17
+ /**
18
+ * Ensure the repair dataset exists by creating it if it doesn't.
19
+ *
20
+ * @param options - The options for ensuring the repair dataset.
21
+ * @returns {Promise<bigint>} - The ID of the created dataset.
22
+ */
23
+ export async function ensureRepairDataset({
24
+ localDb,
25
+ indexerDb,
26
+ client,
27
+ repair,
28
+ }: EnsureRepairDatasetOptions): Promise<bigint> {
29
+ const log = p.taskLog({
30
+ title: 'Ensuring repair dataset',
31
+ })
32
+ const provider = await getPDPProvider(client, {
33
+ providerId: repair.targetProviderId,
34
+ })
35
+
36
+ if (!provider) throw new Error(`Target provider ${repair.targetProviderId} not found or inactive`)
37
+
38
+ let datasetId: bigint | null = null
39
+ // check if dataset already exists
40
+ const existingDatasetId = await getRepairDataset({
41
+ indexerDb,
42
+ providerId: repair.targetProviderId,
43
+ payer: client.account.address,
44
+ })
45
+
46
+ if (existingDatasetId) {
47
+ datasetId = existingDatasetId
48
+ log.success(`Data set #${datasetId} already exists at ${provider.pdp.serviceURL}`)
49
+ } else {
50
+ const { txHash, statusUrl } = await SP.createDataSet(client, {
51
+ payee: provider.payee,
52
+ serviceURL: provider.pdp.serviceURL,
53
+ payer: client.account.address,
54
+ cdn: false,
55
+ metadata: getRepairDatasetMetadata(),
56
+ })
57
+ log.message(`Waiting for data to be created at ${provider.pdp.serviceURL} ${hashLink(txHash, client.chain)}...`)
58
+ const waitForResult = await SP.waitForCreateDataSet({
59
+ statusUrl,
60
+ })
61
+ datasetId = waitForResult.dataSetId
62
+ log.success(`Data set #${datasetId} created at ${provider.pdp.serviceURL}`)
63
+ }
64
+ await repairUpdate({
65
+ localDb,
66
+ repairId: repair.id,
67
+ targetDataSetId: datasetId,
68
+ })
69
+ return datasetId
70
+ }
@@ -0,0 +1,255 @@
1
+ import { taskLog } from '@clack/prompts'
2
+ import * as Piece from '@filoz/synapse-core/piece'
3
+ import { createPieceUrlPDP } from '@filoz/synapse-core/piece'
4
+ import * as SP from '@filoz/synapse-core/sp'
5
+ import { and, asc, eq, gt, inArray } from 'drizzle-orm'
6
+ import PQueue from 'p-queue'
7
+ import { getTargetDataset } from '../db/get-target-dataset.ts'
8
+ import { syncPiecesOnchain } from '../db/sync-pieces-onchain.ts'
9
+ import { updateOperation } from '../db/update-operation.ts'
10
+ import { upsertOperations } from '../db/upsert-operations.ts'
11
+ import type { OperationSelect, RepairSelect } from '../local-schema.ts'
12
+ import type { IndexerDatabase, LocalDatabase, WalletClient } from '../types.ts'
13
+ import { hashLink } from '../utils.ts'
14
+
15
+ /** Pending `add_piece` operations batched for a single pull job. */
16
+ export type PullPiecesBatch = {
17
+ operations: OperationSelect[]
18
+ }
19
+
20
+ export type RunPullPiecesPhaseOptions = {
21
+ localDb: LocalDatabase
22
+ indexerDb: IndexerDatabase
23
+ repair: RepairSelect
24
+ concurrency: number
25
+ batchSize: number
26
+ client: WalletClient
27
+ reset: boolean
28
+ }
29
+
30
+ /** Mock pull worker: logs each batch and its piece CIDs. */
31
+ export function createPullPiecesWorker({
32
+ localDb,
33
+ indexerDb,
34
+ repair,
35
+ client,
36
+ state,
37
+ log,
38
+ }: {
39
+ localDb: LocalDatabase
40
+ indexerDb: IndexerDatabase
41
+ repair: RepairSelect
42
+ client: WalletClient
43
+ state: {
44
+ totalBatches: number
45
+ totalOperations: number
46
+ completedOperations: number
47
+ failedOperations: number
48
+ }
49
+ log: ReturnType<typeof taskLog>
50
+ }) {
51
+ return async (batch: PullPiecesBatch, batchNumber: number) => {
52
+ let completedCids = 0
53
+ let failedCids = 0
54
+ const cidToOperation = new Map<string, OperationSelect>()
55
+
56
+ const spin = log.group(`Batch ${batchNumber}/${state.totalBatches}`)
57
+ spin.message(`Pull 0 completed, 0 failed`)
58
+
59
+ try {
60
+ const dataset = await getTargetDataset({ localDb, repairId: repair.id, client })
61
+
62
+ for (const operation of batch.operations) {
63
+ cidToOperation.set(operation.cid, operation)
64
+ }
65
+
66
+ // sync pieces onchain to avoid duplicates
67
+ const completedOperations1 = await syncPiecesOnchain({
68
+ indexerDb,
69
+ localDb,
70
+ dataSetId: dataset.dataSetId,
71
+ cidToOperation,
72
+ })
73
+ state.completedOperations += completedOperations1
74
+ completedCids += completedOperations1
75
+
76
+ // create pull pieces
77
+ const pullPieces: SP.PullPieceInput[] = []
78
+ for (const [cid, operation] of cidToOperation) {
79
+ const pieceCid = Piece.from(cid)
80
+ const sourceUrl = createPieceUrlPDP({
81
+ cid,
82
+ serviceURL: operation.alternateProvider,
83
+ })
84
+ pullPieces.push({ pieceCid, sourceUrl })
85
+ }
86
+
87
+ if (pullPieces.length > 0) {
88
+ // wait for pull pieces
89
+ const pullResult = await SP.waitForPullPieces(client, {
90
+ serviceURL: repair.targetProviderUrl,
91
+ dataSetId: dataset.dataSetId,
92
+ clientDataSetId: dataset.clientDataSetId,
93
+ pieces: pullPieces,
94
+ timeout: 1000 * 60 * 30,
95
+ onStatus: (_status) => {
96
+ const completed = _status.pieces.filter((piece) => piece.status === 'complete').length
97
+ const failed = _status.pieces.filter((piece) => piece.status === 'failed').length
98
+ spin.message(`Pull ${completed} completed, ${failed} failed`)
99
+ },
100
+ })
101
+
102
+ for (const { pieceCid, status } of pullResult.pieces) {
103
+ const cid = pieceCid.toString()
104
+ const operation = cidToOperation.get(cid)
105
+ if (!operation) {
106
+ console.log(`operation not found for cid ${cid}`)
107
+ continue
108
+ }
109
+
110
+ if (status !== 'complete') {
111
+ state.failedOperations++
112
+ failedCids++
113
+ cidToOperation.delete(cid)
114
+ await updateOperation({
115
+ localDb,
116
+ operationId: operation.id,
117
+ status: 'failed',
118
+ error: `pull failed with status ${status}`,
119
+ })
120
+ }
121
+ }
122
+ }
123
+
124
+ // sync against indexer to avoid duplicates
125
+ const completedOperations2 = await syncPiecesOnchain({
126
+ indexerDb,
127
+ localDb,
128
+ dataSetId: dataset.dataSetId,
129
+ cidToOperation,
130
+ })
131
+ state.completedOperations += completedOperations2
132
+ completedCids += completedOperations2
133
+
134
+ const commitPieces: SP.addPieces.PieceType[] = []
135
+ for (const [cid] of cidToOperation) {
136
+ commitPieces.push({
137
+ pieceCid: Piece.from(cid),
138
+ })
139
+ }
140
+
141
+ if (commitPieces.length > 0) {
142
+ const addPiecesResult = await SP.addPieces(client, {
143
+ serviceURL: repair.targetProviderUrl,
144
+ dataSetId: dataset.dataSetId,
145
+ clientDataSetId: dataset.clientDataSetId,
146
+ pieces: commitPieces,
147
+ })
148
+
149
+ spin.message(`Waiting for add pieces ${hashLink(addPiecesResult.txHash, client.chain)}...`)
150
+ const addPiecesResult2 = await SP.waitForAddPieces(addPiecesResult)
151
+ state.completedOperations += cidToOperation.size
152
+ completedCids += cidToOperation.size
153
+ await upsertOperations({
154
+ localDb,
155
+ operations: Array.from(cidToOperation.values()).map((operation) => ({
156
+ ...operation,
157
+ status: 'completed',
158
+ error: null,
159
+ result: { dataSetId: addPiecesResult2.dataSetId, txHash: addPiecesResult2.txHash },
160
+ })),
161
+ })
162
+ }
163
+ spin.success(`Batch ${batchNumber}/${state.totalBatches} ${completedCids} added, ${failedCids} failed`)
164
+ } catch (error) {
165
+ state.failedOperations += cidToOperation.size
166
+ const message = error instanceof Error ? error.message : 'Unknown error'
167
+ spin.error(`Batch ${batchNumber}/${state.totalBatches} - ${message.replace(/\n/g, ' ')}`)
168
+ await upsertOperations({
169
+ localDb,
170
+ operations: Array.from(cidToOperation.values()).map((operation) => ({
171
+ ...operation,
172
+ status: 'failed',
173
+ error: message,
174
+ })),
175
+ })
176
+ }
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Pull pending `add_piece` operations without loading the whole repair into memory.
182
+ *
183
+ * Pending piece operations are fetched lazily and queued with bounded backpressure. Failed piece
184
+ * operations are intentionally skipped unless `reset` is set.
185
+ */
186
+ export async function runPullPiecesPhase({
187
+ localDb,
188
+ indexerDb,
189
+ repair,
190
+ concurrency,
191
+ batchSize,
192
+ client,
193
+ reset,
194
+ }: RunPullPiecesPhaseOptions): Promise<void> {
195
+ const localSchema = localDb._.fullSchema
196
+ const pullConcurrency = Math.max(1, concurrency)
197
+ const pullBatchSize = Math.max(1, batchSize)
198
+ let pullCursor = 0
199
+
200
+ const totalOperations = await localDb.$count(
201
+ localSchema.operations,
202
+ and(
203
+ eq(localSchema.operations.repairId, repair.id),
204
+ eq(localSchema.operations.type, 'add_piece'),
205
+ inArray(localSchema.operations.status, reset ? ['pending', 'failed'] : ['pending'])
206
+ )
207
+ )
208
+ let batchNumber = 0
209
+ const state = {
210
+ totalBatches: Math.ceil(totalOperations / pullBatchSize),
211
+ totalOperations,
212
+ completedOperations: 0,
213
+ failedOperations: 0,
214
+ }
215
+
216
+ const log = taskLog({
217
+ title: 'Pulling pieces',
218
+ limit: 1,
219
+ })
220
+
221
+ async function getNextPullBatch(): Promise<PullPiecesBatch | null> {
222
+ const operations = await localDb.query.operations.findMany({
223
+ where: and(
224
+ eq(localSchema.operations.repairId, repair.id),
225
+ eq(localSchema.operations.type, 'add_piece'),
226
+ inArray(localSchema.operations.status, reset ? ['pending', 'failed'] : ['pending']),
227
+ gt(localSchema.operations.id, pullCursor)
228
+ ),
229
+ orderBy: [asc(localSchema.operations.id)],
230
+ limit: pullBatchSize,
231
+ })
232
+ if (operations.length === 0) {
233
+ return null
234
+ }
235
+
236
+ pullCursor = operations.at(-1)?.id ?? pullCursor
237
+ return { operations }
238
+ }
239
+
240
+ const pullPiecesWorker = createPullPiecesWorker({ localDb, indexerDb, repair, client, state, log })
241
+ const pullPiecesQueue = new PQueue({ concurrency: pullConcurrency })
242
+
243
+ while (true) {
244
+ await pullPiecesQueue.onSizeLessThan(pullConcurrency)
245
+ const batch = await getNextPullBatch()
246
+ if (!batch) break
247
+ batchNumber++
248
+ const currentBatchNumber = batchNumber
249
+ pullPiecesQueue.add(() => pullPiecesWorker(batch, currentBatchNumber)).catch(console.error)
250
+ }
251
+
252
+ await pullPiecesQueue.onIdle()
253
+
254
+ log.success(`Pulled ${state.completedOperations} pieces, ${state.failedOperations} failed`)
255
+ }
package/src/types.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { Chain } from '@filoz/synapse-core/chains'
2
+ import type { Client } from '@libsql/client'
3
+ import type { LibSQLDatabase } from 'drizzle-orm/libsql'
4
+ import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
5
+ import type { z } from 'incur'
6
+ import type { Pool } from 'pg'
7
+ import type { Account, Address, Hex, Transport, Client as ViemClient } from 'viem'
8
+ import type * as indexerSchema from './indexer-schema.ts'
9
+ import type * as localSchema from './local-schema.ts'
10
+ import type { contextSchema } from './middleware.ts'
11
+ export type LocalDatabase = LibSQLDatabase<typeof localSchema> & {
12
+ $client: Client
13
+ }
14
+
15
+ export type IndexerDatabase = NodePgDatabase<typeof indexerSchema> & {
16
+ $client: Pool
17
+ }
18
+
19
+ export interface Config {
20
+ privateKey: Hex
21
+ indexerMainnetUrl: string
22
+ indexerCalibrationUrl: string
23
+ chainId: number
24
+ dbPath: string
25
+ }
26
+
27
+ export type WalletClient = ViemClient<Transport, Chain, Account>
28
+
29
+ export type Context = z.infer<typeof contextSchema>
30
+
31
+ /**
32
+ * Provider details used for repair selection and CID replica lookup.
33
+ */
34
+ export type RepairProvider = {
35
+ providerId: bigint
36
+ providerAddress: Address
37
+ name: string
38
+ serviceUrl: string
39
+ approved: boolean
40
+ endorsed: boolean
41
+ }