@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
@@ -0,0 +1,189 @@
1
+ import * as Piece from '@filoz/synapse-core/piece'
2
+ import { and, asc, eq, isNull, lte, or } from 'drizzle-orm'
3
+ import pMap from 'p-map'
4
+ import type { OperationInsert } from '../local-schema.ts'
5
+ import type { IndexerDatabase } from '../types.ts'
6
+ import { getProvidersByCid } from './get-providers-by-cid.ts'
7
+
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
14
+ /** Source provider whose pieces are being repaired. */
15
+ providerId: bigint
16
+ /** Local repair row to attach operations to. */
17
+ repairId: number
18
+ /** Chain block number captured when the repair was created. */
19
+ 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
+ }
42
+
43
+ /** Options for {@link forEachPiecesPage}; pagination state is managed internally. */
44
+ export type ForEachPiecesPageOptions = Omit<GetPiecesPageOptions, 'offset' | 'seenCids'>
45
+
46
+ type PieceForOperation = {
47
+ cid: string
48
+ metadata: Record<string, string> | null
49
+ }
50
+
51
+ /** Empty CID set for starting a paginated piece walk. */
52
+ export function emptySeenCids(): Set<string> {
53
+ return new Set()
54
+ }
55
+
56
+ /**
57
+ * Fetch one page of pieces for a provider at the repair block and map them to `add_piece` operations.
58
+ *
59
+ * Pieces are read from the indexer in stable dataset/piece order. CIDs are deduped globally so a
60
+ * piece stored under multiple source datasets is queued once into the single IPFS repair dataset.
61
+ * Alternate providers are resolved in one batch per page; operations without alternates are
62
+ * inserted as `skipped` with a descriptive error.
63
+ *
64
+ * Pass `seenCids` and `nextOffset` from the prior result to continue pagination.
65
+ *
66
+ * @param options - Indexer connection, repair context, and optional pagination state.
67
+ * @returns Operations for this page plus pagination cursors.
68
+ */
69
+ export async function getPiecesPage({
70
+ indexerDb,
71
+ providerId,
72
+ repairId,
73
+ blockNumber,
74
+ limit = DEFAULT_PIECES_PAGE_SIZE,
75
+ offset = 0,
76
+ seenCids = emptySeenCids(),
77
+ }: GetPiecesPageOptions): Promise<GetPiecesPageResult> {
78
+ const schema = indexerDb._.fullSchema
79
+ const rows = await indexerDb
80
+ .select({
81
+ cid: schema.pieces.cid,
82
+ metadata: schema.pieces.metadata,
83
+ })
84
+ .from(schema.pieces)
85
+ .innerJoin(schema.dataSets, eq(schema.pieces.dataSetId, schema.dataSets.dataSetId))
86
+ .where(
87
+ and(
88
+ eq(schema.dataSets.providerId, providerId),
89
+ eq(schema.dataSets.deleted, false),
90
+ or(isNull(schema.dataSets.pdpEndEpoch), lte(schema.dataSets.pdpEndEpoch, blockNumber)),
91
+ eq(schema.pieces.removed, false)
92
+ )
93
+ )
94
+ .orderBy(asc(schema.pieces.dataSetId), asc(schema.pieces.pieceId))
95
+ .limit(limit)
96
+ .offset(offset)
97
+
98
+ const now = Date.now()
99
+ const pieces: PieceForOperation[] = []
100
+
101
+ for (const { cid, metadata } of rows) {
102
+ // Same CID can appear on multiple source datasets; only repair it once.
103
+ if (seenCids.has(cid)) continue
104
+ seenCids.add(cid)
105
+
106
+ pieces.push({ cid, metadata })
107
+ }
108
+
109
+ // Resolve pull sources in one query per page; exclude the provider being repaired from alternates.
110
+ const providersByCid = await getProvidersByCid({
111
+ indexerDb,
112
+ cids: pieces.map((piece) => piece.cid),
113
+ excludedProviderIds: [],
114
+ blockNumber,
115
+ })
116
+
117
+ const operations: OperationInsert[] = await pMap(
118
+ pieces,
119
+ async ({ cid, metadata }) => {
120
+ const alternateProviders = providersByCid[cid]?.map((provider) => provider.serviceUrl) ?? []
121
+ let skippedMessage = ''
122
+ let validProvider: string | undefined
123
+ if (alternateProviders.length > 0) {
124
+ validProvider = await Piece.findPieceOnProviders(alternateProviders, Piece.from(cid))
125
+
126
+ if (!validProvider) {
127
+ skippedMessage = `Found ${alternateProviders.length} alternate providers, but none are valid. ${alternateProviders.join(', ')}`
128
+ }
129
+ } else {
130
+ skippedMessage = `No alternate providers found`
131
+ }
132
+
133
+ return {
134
+ repairId,
135
+ type: 'add_piece',
136
+ // Cannot pull without another replica; mark skipped up front so run skips these ops.
137
+ status: validProvider ? 'pending' : 'skipped',
138
+ cid,
139
+ metadata: metadata ?? {},
140
+ alternateProvider: validProvider ?? '',
141
+ error: validProvider ? undefined : skippedMessage,
142
+ createdAt: now,
143
+ updatedAt: now,
144
+ }
145
+ },
146
+ { concurrency: 20 }
147
+ )
148
+
149
+ // )
150
+
151
+ return {
152
+ operations,
153
+ // A full page means there may be more rows; a short page ends pagination.
154
+ hasMore: rows.length === limit,
155
+ nextOffset: offset + rows.length,
156
+ seenCids,
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Walk every page of `add_piece` operations for a provider, invoking `onPage` per batch.
162
+ *
163
+ * Manages `offset` and `seenCids` across pages so callers only handle inserts.
164
+ *
165
+ * @param options - Same inputs as {@link getPiecesPage} except pagination cursors.
166
+ * @param onPage - Async handler for each page result (e.g. batch insert into local DB).
167
+ */
168
+ export async function forEachPiecesPage(
169
+ options: ForEachPiecesPageOptions,
170
+ onPage: (page: GetPiecesPageResult) => Promise<void>
171
+ ): Promise<void> {
172
+ let offset = 0
173
+ let hasMore = true
174
+ let seenCids = emptySeenCids()
175
+
176
+ while (hasMore) {
177
+ const page = await getPiecesPage({
178
+ ...options,
179
+ offset,
180
+ seenCids,
181
+ })
182
+
183
+ await onPage(page)
184
+
185
+ offset = page.nextOffset
186
+ seenCids = page.seenCids
187
+ hasMore = page.hasMore
188
+ }
189
+ }
@@ -0,0 +1,75 @@
1
+ import { and, asc, eq, inArray, isNull, lte, notInArray, or } from 'drizzle-orm'
2
+ import type { IndexerDatabase, RepairProvider } from '../types.ts'
3
+
4
+ export type GetProvidersByCidOptions = {
5
+ indexerDb: IndexerDatabase
6
+ cids: readonly string[]
7
+ excludedProviderIds: readonly bigint[]
8
+ blockNumber: bigint
9
+ }
10
+
11
+ /**
12
+ * Map of piece CID to providers that store that CID at the repair block.
13
+ */
14
+ export type ProvidersByCid = Record<string, RepairProvider[]>
15
+
16
+ /**
17
+ * Find alternate providers for each CID, excluding the given provider IDs.
18
+ *
19
+ * Deleted datasets and removed pieces are ignored. Only approved or endorsed providers are
20
+ * included. Every requested CID is present in the result; CIDs with no alternate providers
21
+ * map to an empty array.
22
+ */
23
+ export async function getProvidersByCid({
24
+ indexerDb,
25
+ cids,
26
+ excludedProviderIds,
27
+ blockNumber,
28
+ }: GetProvidersByCidOptions): Promise<ProvidersByCid> {
29
+ const schema = indexerDb._.fullSchema
30
+ const providersByCid = Object.fromEntries(cids.map((cid) => [cid, []])) as ProvidersByCid
31
+ if (cids.length === 0) return providersByCid
32
+
33
+ const filters = [
34
+ inArray(schema.pieces.cid, [...cids]),
35
+ eq(schema.dataSets.deleted, false),
36
+ or(isNull(schema.dataSets.pdpEndEpoch), lte(schema.dataSets.pdpEndEpoch, blockNumber)),
37
+ eq(schema.pieces.removed, false),
38
+ // or(eq(schema.providers.approved, true), eq(schema.providers.endorsed, true)),
39
+ ]
40
+ if (excludedProviderIds.length > 0) {
41
+ filters.push(notInArray(schema.dataSets.providerId, [...excludedProviderIds]))
42
+ }
43
+
44
+ // Join through datasets because providers own datasets, while pieces only reference dataset IDs.
45
+ const rows = await indexerDb
46
+ .selectDistinct({
47
+ cid: schema.pieces.cid,
48
+ providerId: schema.providers.providerId,
49
+ providerAddress: schema.providers.providerAddress,
50
+ name: schema.providers.name,
51
+ serviceUrl: schema.providers.serviceUrl,
52
+ approved: schema.providers.approved,
53
+ endorsed: schema.providers.endorsed,
54
+ })
55
+ .from(schema.pieces)
56
+ .innerJoin(schema.dataSets, eq(schema.pieces.dataSetId, schema.dataSets.dataSetId))
57
+ .innerJoin(schema.providers, eq(schema.dataSets.providerId, schema.providers.providerId))
58
+ .where(and(...filters))
59
+ .orderBy(asc(schema.pieces.cid), asc(schema.providers.providerId))
60
+
61
+ for (const { cid, ...provider } of rows) {
62
+ if (provider.providerAddress && provider.serviceUrl && provider.name) {
63
+ providersByCid[cid]?.push({
64
+ providerId: provider.providerId,
65
+ providerAddress: provider.providerAddress,
66
+ name: provider.name,
67
+ serviceUrl: provider.serviceUrl,
68
+ approved: provider.approved,
69
+ endorsed: provider.endorsed,
70
+ })
71
+ }
72
+ }
73
+
74
+ return providersByCid
75
+ }
@@ -0,0 +1,44 @@
1
+ import { and, asc, eq, isNull } from 'drizzle-orm'
2
+ import type { Address } from 'viem'
3
+ import type { IndexerDatabase } from '../types.ts'
4
+ import { EARLY_REPAIR_SOURCE } from '../utils.ts'
5
+
6
+ export type GetRepairDatasetOptions = {
7
+ indexerDb: IndexerDatabase
8
+ providerId: bigint
9
+ payer: Address
10
+ }
11
+
12
+ /**
13
+ * Find one IPFS-enabled repair dataset for a provider at the repair block, if it exists.
14
+ *
15
+ * When multiple datasets match, the lowest `dataSetId` is returned.
16
+ */
17
+ export async function getRepairDataset({
18
+ indexerDb,
19
+ providerId,
20
+ payer,
21
+ }: GetRepairDatasetOptions): Promise<bigint | null> {
22
+ const schema = indexerDb._.fullSchema
23
+
24
+ const [row] = await indexerDb
25
+ .select({
26
+ dataSetId: schema.dataSets.dataSetId,
27
+ })
28
+ .from(schema.dataSets)
29
+ .where(
30
+ and(
31
+ eq(schema.dataSets.providerId, providerId),
32
+ eq(schema.dataSets.deleted, false),
33
+ isNull(schema.dataSets.pdpEndEpoch),
34
+ eq(schema.dataSets.payer, payer.toLowerCase()),
35
+ eq(schema.dataSets.source, EARLY_REPAIR_SOURCE),
36
+ eq(schema.dataSets.withCdn, false),
37
+ eq(schema.dataSets.withIpfsIndexing, true)
38
+ )
39
+ )
40
+ .orderBy(asc(schema.dataSets.dataSetId))
41
+ .limit(1)
42
+
43
+ return row.dataSetId ?? null
44
+ }
@@ -0,0 +1,47 @@
1
+ import { and, eq } from 'drizzle-orm'
2
+ import type { Context, RepairProvider } from '../types.ts'
3
+
4
+ export interface GetRepairProviderOptions extends Pick<Context, 'indexerDb'> {
5
+ providerId: bigint
6
+ }
7
+
8
+ /**
9
+ * Load an active provider by ID for use as a repair target.
10
+ */
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)
34
+
35
+ if (!provider?.providerAddress || !provider?.serviceUrl || !provider?.name) {
36
+ return null
37
+ }
38
+
39
+ return {
40
+ providerId: provider.providerId,
41
+ providerAddress: provider.providerAddress,
42
+ name: provider.name,
43
+ serviceUrl: provider.serviceUrl,
44
+ approved: provider.approved,
45
+ endorsed: provider.endorsed,
46
+ }
47
+ }
@@ -0,0 +1,47 @@
1
+ import { getDataSet } from '@filoz/synapse-core/warm-storage'
2
+ import { eq } from 'drizzle-orm'
3
+ import { MissingRepairDataSetError, RepairNotFoundError } from '../error.ts'
4
+ import type { LocalDatabase, WalletClient } from '../types.ts'
5
+
6
+ const targetDatasetCache = new Map<number, getDataSet.OutputType>()
7
+
8
+ /**
9
+ * Get the single IPFS-enabled target dataset for a repair.
10
+ *
11
+ * @param options - The options for getting the target dataset.
12
+ */
13
+ export async function getTargetDataset({
14
+ localDb,
15
+ repairId,
16
+ client,
17
+ }: {
18
+ localDb: LocalDatabase
19
+ repairId: number
20
+ client: WalletClient
21
+ }) {
22
+ const cached = targetDatasetCache.get(repairId)
23
+ if (cached) {
24
+ return cached
25
+ }
26
+
27
+ const repair = await localDb.query.repairs.findFirst({
28
+ where: eq(localDb._.fullSchema.repairs.id, repairId),
29
+ columns: { targetDataSetId: true },
30
+ })
31
+ if (!repair) {
32
+ throw new RepairNotFoundError(repairId)
33
+ }
34
+
35
+ if (repair.targetDataSetId == null) {
36
+ throw new MissingRepairDataSetError()
37
+ }
38
+
39
+ const dataSet = await getDataSet(client, { dataSetId: repair.targetDataSetId })
40
+ if (!dataSet) {
41
+ throw new MissingRepairDataSetError()
42
+ }
43
+
44
+ targetDatasetCache.set(repairId, dataSet)
45
+
46
+ return dataSet
47
+ }
@@ -0,0 +1,101 @@
1
+ import { taskLog } from '@clack/prompts'
2
+ import { getBlockNumber } from 'viem/actions'
3
+ import { NoAlternateProviderError, RepairCreationError } from '../error.ts'
4
+ import type { Context } from '../types.ts'
5
+ import { forEachPiecesPage } from './get-pieces.ts'
6
+ import { getRepairProvider } from './get-repair-provider.ts'
7
+
8
+ export interface RepairCreateOptions extends Context {
9
+ repairProviderId: bigint
10
+ targetProviderId: bigint
11
+ }
12
+
13
+ /**
14
+ * Prepare a repair by selecting a target provider, creating the repair row, and
15
+ * inserting pending dataset and piece operations.
16
+ *
17
+ * @param {RepairCreateOptions} options - The options for creating a repair.
18
+ * @returns {Promise<number>} The ID of the created repair.
19
+ */
20
+ export async function repairCreate(options: RepairCreateOptions): Promise<number> {
21
+ const { indexerDb, localDb, repairProviderId, targetProviderId, client } = options
22
+ const localSchema = localDb._.fullSchema
23
+ const now = Date.now()
24
+ const blockNumber = await getBlockNumber(client)
25
+
26
+ const log = taskLog({
27
+ title: 'Creating repair',
28
+ limit: 10,
29
+ retainLog: true,
30
+ })
31
+
32
+ // Load the explicit target provider.
33
+ if (targetProviderId === repairProviderId) {
34
+ throw new RepairCreationError('Target provider must differ from the provider being repaired')
35
+ }
36
+ const targetProvider = await getRepairProvider({
37
+ indexerDb,
38
+ providerId: targetProviderId,
39
+ })
40
+
41
+ if (!targetProvider) {
42
+ throw new NoAlternateProviderError(targetProviderId)
43
+ }
44
+
45
+ // Create the repair row
46
+ const [repair] = await localDb
47
+ .insert(localSchema.repairs)
48
+ .values({
49
+ repairProviderId,
50
+ targetProviderId: targetProvider.providerId,
51
+ targetProviderUrl: targetProvider.serviceUrl,
52
+ targetDataSetId: null,
53
+ blockNumber,
54
+ createdAt: now,
55
+ updatedAt: now,
56
+ })
57
+ .returning({ id: localSchema.repairs.id })
58
+
59
+ if (!repair) throw new RepairCreationError()
60
+
61
+ // Add the pieces to the repair
62
+ let totalOperations = 0
63
+ let totalPendingOperations = 0
64
+ let totalSkippedOperations = 0
65
+ const seenCids = new Set<string>()
66
+ await forEachPiecesPage(
67
+ {
68
+ indexerDb,
69
+ providerId: repairProviderId,
70
+ repairId: repair.id,
71
+ blockNumber,
72
+ },
73
+ async (page) => {
74
+ for (const operation of page.operations) {
75
+ if (seenCids.has(operation.cid)) {
76
+ continue
77
+ }
78
+ seenCids.add(operation.cid)
79
+ }
80
+ const pendingOperations = page.operations.filter((operation) => operation.status === 'pending').length
81
+ const skippedOperations = page.operations.filter((operation) => operation.status === 'skipped').length
82
+ totalOperations += page.operations.length
83
+ totalPendingOperations += pendingOperations
84
+ totalSkippedOperations += skippedOperations
85
+
86
+ if (page.operations.length > 0) {
87
+ await localDb.insert(localSchema.operations).values(page.operations)
88
+ }
89
+
90
+ log.message(
91
+ `Inserted ${page.operations.length} operations (${pendingOperations} pending, ${skippedOperations} skipped)`
92
+ )
93
+ }
94
+ )
95
+
96
+ log.success(
97
+ `Created repair ${repair.id} with ${totalOperations} operations (${totalPendingOperations} pending, ${totalSkippedOperations} skipped)`,
98
+ { showLog: true }
99
+ )
100
+ return repair.id
101
+ }
@@ -0,0 +1,39 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import * as localSchema from '../local-schema.ts'
3
+ import type { LocalDatabase } from '../types.ts'
4
+
5
+ export type RepairDeleteOptions = {
6
+ localDb: LocalDatabase
7
+ repairId: number
8
+ }
9
+
10
+ export type RepairDeleteResult = {
11
+ deleted: boolean
12
+ operationsDeleted: number
13
+ }
14
+
15
+ /**
16
+ * Delete a repair and all of its operations from the local database.
17
+ */
18
+ export async function repairDelete({ localDb, repairId }: RepairDeleteOptions): Promise<RepairDeleteResult> {
19
+ const repair = await localDb.query.repairs.findFirst({
20
+ where: eq(localSchema.repairs.id, repairId),
21
+ columns: { id: true },
22
+ with: {
23
+ operations: {
24
+ columns: { id: true },
25
+ },
26
+ },
27
+ })
28
+
29
+ if (!repair) {
30
+ return { deleted: false, operationsDeleted: 0 }
31
+ }
32
+
33
+ const operationsDeleted = await localDb
34
+ .delete(localSchema.operations)
35
+ .where(eq(localSchema.operations.repairId, repairId))
36
+ await localDb.delete(localSchema.repairs).where(eq(localSchema.repairs.id, repairId))
37
+
38
+ return { deleted: true, operationsDeleted: operationsDeleted.rowsAffected }
39
+ }
@@ -0,0 +1,20 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import type { RepairUpdate } from '../local-schema.ts'
3
+ import * as localSchema from '../local-schema.ts'
4
+ import type { LocalDatabase } from '../types.ts'
5
+
6
+ export type RepairUpdateOptions = {
7
+ localDb: LocalDatabase
8
+ repairId: number
9
+ status?: localSchema.RepairStatus
10
+ targetDataSetId?: bigint | null
11
+ }
12
+
13
+ export async function repairUpdate({ localDb, repairId, status, targetDataSetId }: RepairUpdateOptions) {
14
+ const update: RepairUpdate = {
15
+ updatedAt: Date.now(),
16
+ }
17
+ if (status) update.status = status
18
+ if (targetDataSetId !== undefined) update.targetDataSetId = targetDataSetId
19
+ await localDb.update(localSchema.repairs).set(update).where(eq(localSchema.repairs.id, repairId))
20
+ }
@@ -0,0 +1,53 @@
1
+ import { and, eq, inArray } from 'drizzle-orm'
2
+ import type { OperationInsert, OperationSelect } from '../local-schema.ts'
3
+ import type { IndexerDatabase, LocalDatabase } from '../types.ts'
4
+ import { upsertOperations } from './upsert-operations.ts'
5
+
6
+ export type SyncPiecesOnchainOptions = {
7
+ indexerDb: IndexerDatabase
8
+ localDb: LocalDatabase
9
+ dataSetId: bigint
10
+ cidToOperation: Map<string, OperationSelect>
11
+ }
12
+
13
+ /**
14
+ * Sync pieces onchain to avoid duplicates.
15
+ */
16
+ export async function syncPiecesOnchain({ indexerDb, localDb, dataSetId, cidToOperation }: SyncPiecesOnchainOptions) {
17
+ const cids = Array.from(cidToOperation.keys())
18
+ const schema = indexerDb._.fullSchema
19
+ let completedOperations = 0
20
+ const rows = await indexerDb
21
+ .select({ cid: schema.pieces.cid })
22
+ .from(schema.pieces)
23
+ .where(
24
+ and(eq(schema.pieces.dataSetId, dataSetId), eq(schema.pieces.removed, false), inArray(schema.pieces.cid, cids))
25
+ )
26
+
27
+ const existingCids = new Set<string>()
28
+ const completedOperation: OperationInsert[] = []
29
+
30
+ for (const row of rows) {
31
+ const operation = cidToOperation.get(row.cid)
32
+ if (!operation) {
33
+ continue
34
+ }
35
+ completedOperation.push({
36
+ ...operation,
37
+ status: 'completed',
38
+ error: null,
39
+ })
40
+ existingCids.add(row.cid)
41
+ cidToOperation.delete(row.cid)
42
+ completedOperations++
43
+ }
44
+
45
+ if (completedOperation.length > 0) {
46
+ await upsertOperations({
47
+ localDb,
48
+ operations: completedOperation,
49
+ })
50
+ }
51
+
52
+ return completedOperations
53
+ }
@@ -0,0 +1,26 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import * as localSchema from '../local-schema.ts'
3
+ import type { LocalDatabase } from '../types.ts'
4
+
5
+ export type UpdateOperationOptions = {
6
+ localDb: LocalDatabase
7
+ operationId: number
8
+ status: localSchema.OperationStatus
9
+ result?: localSchema.OperationResult | null
10
+ error?: string | null
11
+ }
12
+
13
+ /**
14
+ * Updates an operation in the database.
15
+ */
16
+ export async function updateOperation({ localDb, operationId, status, result, error }: UpdateOperationOptions) {
17
+ await localDb
18
+ .update(localSchema.operations)
19
+ .set({
20
+ status,
21
+ result,
22
+ error: error ?? null,
23
+ updatedAt: Date.now(),
24
+ })
25
+ .where(eq(localSchema.operations.id, operationId))
26
+ }
@@ -0,0 +1,23 @@
1
+ import type { OperationInsert } from '../local-schema.ts'
2
+ import * as localSchema from '../local-schema.ts'
3
+ import type { LocalDatabase } from '../types.ts'
4
+ import { buildConflictUpdateColumns } from '../utils.ts'
5
+
6
+ export type UpsertOperationsOptions = {
7
+ localDb: LocalDatabase
8
+ operations: OperationInsert[]
9
+ }
10
+
11
+ /**
12
+ * Upserts operations in the database.
13
+ */
14
+ export async function upsertOperations({ localDb, operations }: UpsertOperationsOptions) {
15
+ const now = Date.now()
16
+ await localDb
17
+ .insert(localDb._.fullSchema.operations)
18
+ .values(operations.map((operation) => ({ ...operation, updatedAt: now })))
19
+ .onConflictDoUpdate({
20
+ target: localDb._.fullSchema.operations.id,
21
+ set: buildConflictUpdateColumns(localSchema.operations, ['status', 'error', 'updatedAt']),
22
+ })
23
+ }