@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.
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +19 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/commands/datasets.d.ts +9 -0
- package/dist/src/commands/datasets.d.ts.map +1 -0
- package/dist/src/commands/datasets.js +59 -0
- package/dist/src/commands/datasets.js.map +1 -0
- package/dist/src/commands/providers.d.ts +9 -0
- package/dist/src/commands/providers.d.ts.map +1 -0
- package/dist/src/commands/providers.js +84 -0
- package/dist/src/commands/providers.js.map +1 -0
- package/dist/src/commands/repair.d.ts +9 -0
- package/dist/src/commands/repair.d.ts.map +1 -0
- package/dist/src/commands/repair.js +170 -0
- package/dist/src/commands/repair.js.map +1 -0
- package/dist/src/commands/setup.d.ts +11 -0
- package/dist/src/commands/setup.d.ts.map +1 -0
- package/dist/src/commands/setup.js +127 -0
- package/dist/src/commands/setup.js.map +1 -0
- package/dist/src/commands/wallet.d.ts +9 -0
- package/dist/src/commands/wallet.d.ts.map +1 -0
- package/dist/src/commands/wallet.js +150 -0
- package/dist/src/commands/wallet.js.map +1 -0
- package/dist/src/db/get-pieces.d.ts +23 -0
- package/dist/src/db/get-pieces.d.ts.map +1 -0
- package/dist/src/db/get-pieces.js +84 -0
- package/dist/src/db/get-pieces.js.map +1 -0
- package/dist/src/db/get-providers-by-cid.d.ts +10 -0
- package/dist/src/db/get-providers-by-cid.d.ts.map +1 -0
- package/dist/src/db/get-providers-by-cid.js +45 -0
- package/dist/src/db/get-providers-by-cid.js.map +1 -0
- package/dist/src/db/get-repair-dataset.d.ts +9 -0
- package/dist/src/db/get-repair-dataset.d.ts.map +1 -0
- package/dist/src/db/get-repair-dataset.js +15 -0
- package/dist/src/db/get-repair-dataset.js.map +1 -0
- package/dist/src/db/get-repair-provider.d.ts +6 -0
- package/dist/src/db/get-repair-provider.d.ts.map +1 -0
- package/dist/src/db/get-repair-provider.js +28 -0
- package/dist/src/db/get-repair-provider.js.map +1 -0
- package/dist/src/db/get-target-dataset.d.ts +7 -0
- package/dist/src/db/get-target-dataset.d.ts.map +1 -0
- package/dist/src/db/get-target-dataset.js +27 -0
- package/dist/src/db/get-target-dataset.js.map +1 -0
- package/dist/src/db/repair-create.d.ts +7 -0
- package/dist/src/db/repair-create.d.ts.map +1 -0
- package/dist/src/db/repair-create.js +69 -0
- package/dist/src/db/repair-create.js.map +1 -0
- package/dist/src/db/repair-delete.d.ts +11 -0
- package/dist/src/db/repair-delete.d.ts.map +1 -0
- package/dist/src/db/repair-delete.js +22 -0
- package/dist/src/db/repair-delete.js.map +1 -0
- package/dist/src/db/repair-update.d.ts +10 -0
- package/dist/src/db/repair-update.d.ts.map +1 -0
- package/dist/src/db/repair-update.js +13 -0
- package/dist/src/db/repair-update.js.map +1 -0
- package/dist/src/db/sync-pieces-onchain.d.ts +10 -0
- package/dist/src/db/sync-pieces-onchain.d.ts.map +1 -0
- package/dist/src/db/sync-pieces-onchain.js +35 -0
- package/dist/src/db/sync-pieces-onchain.js.map +1 -0
- package/dist/src/db/update-operation.d.ts +11 -0
- package/dist/src/db/update-operation.d.ts.map +1 -0
- package/dist/src/db/update-operation.js +14 -0
- package/dist/src/db/update-operation.js.map +1 -0
- package/dist/src/db/upsert-operations.d.ts +8 -0
- package/dist/src/db/upsert-operations.d.ts.map +1 -0
- package/dist/src/db/upsert-operations.js +13 -0
- package/dist/src/db/upsert-operations.js.map +1 -0
- package/dist/src/error.d.ts +14 -0
- package/dist/src/error.d.ts.map +1 -0
- package/dist/src/error.js +27 -0
- package/dist/src/error.js.map +1 -0
- package/dist/src/indexer-schema.d.ts +549 -0
- package/dist/src/indexer-schema.d.ts.map +1 -0
- package/dist/src/indexer-schema.js +56 -0
- package/dist/src/indexer-schema.js.map +1 -0
- package/dist/src/local-schema.d.ts +456 -0
- package/dist/src/local-schema.d.ts.map +1 -0
- package/dist/src/local-schema.js +63 -0
- package/dist/src/local-schema.js.map +1 -0
- package/dist/src/middleware.d.ts +19 -0
- package/dist/src/middleware.d.ts.map +1 -0
- package/dist/src/middleware.js +28 -0
- package/dist/src/middleware.js.map +1 -0
- package/dist/src/pipeline/create-datasets.d.ts +10 -0
- package/dist/src/pipeline/create-datasets.d.ts.map +1 -0
- package/dist/src/pipeline/create-datasets.js +48 -0
- package/dist/src/pipeline/create-datasets.js.map +1 -0
- package/dist/src/pipeline/pull.d.ts +30 -0
- package/dist/src/pipeline/pull.d.ts.map +1 -0
- package/dist/src/pipeline/pull.js +169 -0
- package/dist/src/pipeline/pull.js.map +1 -0
- package/dist/src/types.d.ts +34 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +1 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils.d.ts +11945 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +121 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +135 -0
- package/readme.md +250 -0
- package/src/cli.ts +20 -0
- package/src/commands/datasets.ts +62 -0
- package/src/commands/providers.ts +99 -0
- package/src/commands/repair.ts +177 -0
- package/src/commands/setup.ts +142 -0
- package/src/commands/wallet.ts +159 -0
- package/src/db/get-pieces.ts +189 -0
- package/src/db/get-providers-by-cid.ts +75 -0
- package/src/db/get-repair-dataset.ts +44 -0
- package/src/db/get-repair-provider.ts +47 -0
- package/src/db/get-target-dataset.ts +47 -0
- package/src/db/repair-create.ts +101 -0
- package/src/db/repair-delete.ts +39 -0
- package/src/db/repair-update.ts +20 -0
- package/src/db/sync-pieces-onchain.ts +53 -0
- package/src/db/update-operation.ts +26 -0
- package/src/db/upsert-operations.ts +23 -0
- package/src/error.ts +33 -0
- package/src/indexer-schema.ts +77 -0
- package/src/local-schema.ts +91 -0
- package/src/middleware.ts +34 -0
- package/src/pipeline/create-datasets.ts +70 -0
- package/src/pipeline/pull.ts +255 -0
- package/src/types.ts +41 -0
- 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
|
+
}
|