@filoz/repair-cli 0.2.1 → 0.4.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 (82) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/cli.js +4 -0
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/commands/datasets.d.ts +1 -0
  5. package/dist/src/commands/datasets.d.ts.map +1 -1
  6. package/dist/src/commands/datasets.js +5 -5
  7. package/dist/src/commands/datasets.js.map +1 -1
  8. package/dist/src/commands/providers.d.ts +1 -0
  9. package/dist/src/commands/providers.d.ts.map +1 -1
  10. package/dist/src/commands/repair.d.ts +1 -0
  11. package/dist/src/commands/repair.d.ts.map +1 -1
  12. package/dist/src/commands/repair.js +7 -3
  13. package/dist/src/commands/repair.js.map +1 -1
  14. package/dist/src/commands/replicate.d.ts +1 -0
  15. package/dist/src/commands/replicate.d.ts.map +1 -1
  16. package/dist/src/commands/replicate.js +7 -3
  17. package/dist/src/commands/replicate.js.map +1 -1
  18. package/dist/src/commands/second-copy.d.ts +25 -0
  19. package/dist/src/commands/second-copy.d.ts.map +1 -0
  20. package/dist/src/commands/second-copy.js +175 -0
  21. package/dist/src/commands/second-copy.js.map +1 -0
  22. package/dist/src/commands/session-key.d.ts +25 -0
  23. package/dist/src/commands/session-key.d.ts.map +1 -0
  24. package/dist/src/commands/session-key.js +114 -0
  25. package/dist/src/commands/session-key.js.map +1 -0
  26. package/dist/src/commands/wallet.d.ts +1 -0
  27. package/dist/src/commands/wallet.d.ts.map +1 -1
  28. package/dist/src/commands/wallet.js +75 -26
  29. package/dist/src/commands/wallet.js.map +1 -1
  30. package/dist/src/db/dedupe-cids.d.ts +1 -1
  31. package/dist/src/db/repair-create.d.ts.map +1 -1
  32. package/dist/src/db/repair-create.js +2 -0
  33. package/dist/src/db/repair-create.js.map +1 -1
  34. package/dist/src/db/replicate-create.d.ts.map +1 -1
  35. package/dist/src/db/replicate-create.js +2 -0
  36. package/dist/src/db/replicate-create.js.map +1 -1
  37. package/dist/src/db/second-copy-create.d.ts +10 -0
  38. package/dist/src/db/second-copy-create.d.ts.map +1 -0
  39. package/dist/src/db/second-copy-create.js +104 -0
  40. package/dist/src/db/second-copy-create.js.map +1 -0
  41. package/dist/src/db/update-operation.d.ts +2 -2
  42. package/dist/src/db/update-operation.d.ts.map +1 -1
  43. package/dist/src/db/update-operation.js +2 -2
  44. package/dist/src/db/upsert-operations.js +1 -1
  45. package/dist/src/local-schema.d.ts +48 -11
  46. package/dist/src/local-schema.d.ts.map +1 -1
  47. package/dist/src/local-schema.js +3 -1
  48. package/dist/src/local-schema.js.map +1 -1
  49. package/dist/src/middleware.d.ts +2 -0
  50. package/dist/src/middleware.d.ts.map +1 -1
  51. package/dist/src/middleware.js +2 -0
  52. package/dist/src/middleware.js.map +1 -1
  53. package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
  54. package/dist/src/pipeline/add-pieces.js +17 -3
  55. package/dist/src/pipeline/add-pieces.js.map +1 -1
  56. package/dist/src/pipeline/create-datasets.d.ts +4 -2
  57. package/dist/src/pipeline/create-datasets.d.ts.map +1 -1
  58. package/dist/src/pipeline/create-datasets.js +5 -5
  59. package/dist/src/pipeline/create-datasets.js.map +1 -1
  60. package/dist/src/utils.d.ts +34 -30
  61. package/dist/src/utils.d.ts.map +1 -1
  62. package/dist/src/utils.js +68 -3
  63. package/dist/src/utils.js.map +1 -1
  64. package/package.json +1 -1
  65. package/readme.md +123 -4
  66. package/src/cli.ts +4 -0
  67. package/src/commands/datasets.ts +7 -5
  68. package/src/commands/repair.ts +7 -3
  69. package/src/commands/replicate.ts +7 -3
  70. package/src/commands/second-copy.ts +187 -0
  71. package/src/commands/session-key.ts +121 -0
  72. package/src/commands/wallet.ts +75 -26
  73. package/src/db/repair-create.ts +2 -0
  74. package/src/db/replicate-create.ts +2 -0
  75. package/src/db/second-copy-create.ts +147 -0
  76. package/src/db/update-operation.ts +3 -3
  77. package/src/db/upsert-operations.ts +1 -1
  78. package/src/local-schema.ts +4 -7
  79. package/src/middleware.ts +2 -0
  80. package/src/pipeline/add-pieces.ts +20 -4
  81. package/src/pipeline/create-datasets.ts +20 -5
  82. package/src/utils.ts +95 -5
@@ -6,19 +6,19 @@ export type UpdateOperationOptions = {
6
6
  localDb: LocalDatabase
7
7
  operationId: number
8
8
  status: localSchema.OperationStatus
9
- result?: localSchema.OperationResult | null
9
+ txHash?: string | null
10
10
  error?: string | null
11
11
  }
12
12
 
13
13
  /**
14
14
  * Updates an operation in the database.
15
15
  */
16
- export async function updateOperation({ localDb, operationId, status, result, error }: UpdateOperationOptions) {
16
+ export async function updateOperation({ localDb, operationId, status, txHash, error }: UpdateOperationOptions) {
17
17
  await localDb
18
18
  .update(localSchema.operations)
19
19
  .set({
20
20
  status,
21
- result,
21
+ txHash,
22
22
  error: error ?? null,
23
23
  updatedAt: Date.now(),
24
24
  })
@@ -18,6 +18,6 @@ export async function upsertOperations({ localDb, operations }: UpsertOperations
18
18
  .values(operations.map((operation) => ({ ...operation, updatedAt: now })))
19
19
  .onConflictDoUpdate({
20
20
  target: localDb._.fullSchema.operations.id,
21
- set: buildConflictUpdateColumns(localSchema.operations, ['status', 'error', 'updatedAt', 'result']),
21
+ set: buildConflictUpdateColumns(localSchema.operations, ['status', 'error', 'updatedAt', 'txHash']),
22
22
  })
23
23
  }
@@ -1,5 +1,4 @@
1
1
  import type { MetadataObject } from '@filoz/synapse-core'
2
- import type * as SP from '@filoz/synapse-core/sp'
3
2
  import { relations } from 'drizzle-orm'
4
3
  import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core'
5
4
  import * as t from 'drizzle-orm/sqlite-core'
@@ -7,14 +6,10 @@ import { customType, sqliteTable as table } from 'drizzle-orm/sqlite-core'
7
6
  import * as Json from 'iso-base/json'
8
7
 
9
8
  export type RepairStatus = 'pending' | 'completed' | 'failed'
9
+ export type RepairKind = 'repair' | 'replicate' | 'second_copy'
10
10
  export type OperationStatus = 'pending' | 'completed' | 'failed' | 'skipped'
11
11
  export type OperationType = 'create_dataset' | 'add_piece'
12
12
 
13
- export type OperationResult = Omit<
14
- SP.AddPiecesSuccess,
15
- 'txStatus' | 'addMessageOk' | 'piecesAdded' | 'pieceCount' | 'confirmedPieceIds'
16
- >
17
-
18
13
  /**
19
14
  * Custom type for JSON
20
15
  * It will be used to store JSON data in the database
@@ -50,8 +45,10 @@ export type RepairUpdate = Partial<RepairInsert>
50
45
  export const repairs = table('repairs', {
51
46
  id: t.int().primaryKey({ autoIncrement: true }),
52
47
  status: t.text().$type<RepairStatus>().notNull().default('pending'),
48
+ kind: t.text().$type<RepairKind>().notNull().default('repair'),
53
49
  repairProviderId: bigintType('repair_provider_id').notNull(),
54
50
  repairDataSetId: bigintType('repair_data_set_id'),
51
+ sourceProviderUrl: t.text('source_provider_url'),
55
52
  targetProviderId: bigintType('target_provider_id').notNull(),
56
53
  targetProviderUrl: t.text('target_provider_url').notNull(),
57
54
  targetDataSetId: bigintType('target_data_set_id'),
@@ -74,7 +71,7 @@ export const operations = table('operations', {
74
71
  cid: t.text().notNull(),
75
72
  metadata: jsonType().$type<MetadataObject>().notNull(),
76
73
  alternateProvider: t.text('alternate_provider').notNull(),
77
- result: jsonType().$type<OperationResult>(),
74
+ txHash: t.text('tx_hash'),
78
75
  error: t.text(),
79
76
  createdAt: t.integer('created_at').notNull(),
80
77
  updatedAt: t.integer('updated_at').notNull(),
package/src/middleware.ts CHANGED
@@ -13,6 +13,7 @@ export const contextSchema = z.object({
13
13
  client: z.custom<Client<Transport, Chain, Account>>(),
14
14
  chain: z.custom<Chain>(),
15
15
  source: z.string(),
16
+ isInteractive: z.boolean(),
16
17
  })
17
18
 
18
19
  export const contextMiddleware = middleware<typeof contextSchema>(async (c, next) => {
@@ -38,6 +39,7 @@ export const contextMiddleware = middleware<typeof contextSchema>(async (c, next
38
39
  c.set('client', client)
39
40
  c.set('chain', chain)
40
41
  c.set('source', source)
42
+ c.set('isInteractive', !c.agent && !c.formatExplicit)
41
43
  await next()
42
44
 
43
45
  localDb.$client.close()
@@ -9,7 +9,7 @@ import { repairUpdate } from '../db/repair-update.ts'
9
9
  import { upsertOperations } from '../db/upsert-operations.ts'
10
10
  import type { OperationSelect, RepairSelect } from '../local-schema.ts'
11
11
  import type { IndexerDatabase, LocalDatabase, WalletClient } from '../types.ts'
12
- import { excludeOperationsByCid, hashLink, operationsToPullPieces } from '../utils.ts'
12
+ import { completeConfirmedOperations, excludeOperationsByCid, hashLink, operationsToPullPieces } from '../utils.ts'
13
13
 
14
14
  export type RunPullPiecesPhaseOptions = {
15
15
  localDb: LocalDatabase
@@ -58,6 +58,15 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
58
58
  if (isRepair) {
59
59
  operations = await dedupeCids({ indexerDb, localDb, dataSetId: dataset.dataSetId, operations })
60
60
  }
61
+ // A previous run may have submitted a transaction and crashed before marking rows completed.
62
+ const operationsBeforeConfirmationCheck = operations.length
63
+ operations = await completeConfirmedOperations({ localDb, client, operations })
64
+ const confirmedOperations = operationsBeforeConfirmationCheck - operations.length
65
+ if (confirmedOperations > 0) {
66
+ state.completedOperations += confirmedOperations
67
+ completedOps += confirmedOperations
68
+ }
69
+
61
70
  group.message(`Pulling ${operations.length} pieces...`)
62
71
  // pull pieces
63
72
  if (operations.length > 0) {
@@ -66,7 +75,7 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
66
75
  dataSetId: dataset.dataSetId,
67
76
  clientDataSetId: dataset.clientDataSetId,
68
77
  pieces: operationsToPullPieces(operations),
69
- timeout: 1000 * 60 * 30,
78
+ timeout: 1000 * 60 * 30, // 30 minutes
70
79
  onStatus: (status) => {
71
80
  const completed = status.pieces.filter((piece) => piece.status === 'complete').length
72
81
  const failed = status.pieces.filter((piece) => piece.status === 'failed').length
@@ -105,9 +114,16 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
105
114
  metadata: isRepair ? undefined : operation.metadata,
106
115
  })),
107
116
  })
117
+ await upsertOperations({
118
+ localDb,
119
+ operations: operations.map((operation) => ({
120
+ ...operation,
121
+ txHash: addPiecesResult.txHash,
122
+ })),
123
+ })
108
124
 
109
125
  group.message(`Waiting for add pieces ${hashLink(addPiecesResult.txHash, client.chain)}...`)
110
- const addPiecesResult2 = await SP.waitForAddPieces(addPiecesResult)
126
+ const waitForAddPieces = await SP.waitForAddPieces(addPiecesResult)
111
127
  state.completedOperations += operations.length
112
128
  completedOps += operations.length
113
129
  await upsertOperations({
@@ -116,7 +132,7 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
116
132
  ...operation,
117
133
  status: 'completed',
118
134
  error: null,
119
- result: { dataSetId: addPiecesResult2.dataSetId, txHash: addPiecesResult2.txHash },
135
+ txHash: waitForAddPieces.txHash,
120
136
  })),
121
137
  })
122
138
  }
@@ -2,6 +2,7 @@ import * as p from '@clack/prompts'
2
2
  import * as SP from '@filoz/synapse-core/sp'
3
3
  import { getPDPProvider } from '@filoz/synapse-core/sp-registry'
4
4
  import { eq } from 'drizzle-orm'
5
+ import type { Address } from 'viem'
5
6
  import { findRepairDataset } from '../db/find-repair-dataset.ts'
6
7
  import { repairUpdate } from '../db/repair-update.ts'
7
8
  import type { RepairSelect } from '../local-schema.ts'
@@ -14,6 +15,7 @@ export type EnsureRepairDatasetOptions = {
14
15
  indexerDb: IndexerDatabase
15
16
  client: WalletClient
16
17
  repair: RepairSelect
18
+ payer: Address
17
19
  }
18
20
 
19
21
  /**
@@ -21,7 +23,14 @@ export type EnsureRepairDatasetOptions = {
21
23
  *
22
24
  * @param options - The options for ensuring the repair dataset.
23
25
  */
24
- export async function ensureRepairDataset({ source, localDb, indexerDb, client, repair }: EnsureRepairDatasetOptions) {
26
+ export async function ensureRepairDataset({
27
+ source,
28
+ localDb,
29
+ indexerDb,
30
+ client,
31
+ repair,
32
+ payer,
33
+ }: EnsureRepairDatasetOptions) {
25
34
  const log = p.taskLog({
26
35
  title: 'Ensuring repair dataset',
27
36
  })
@@ -36,7 +45,7 @@ export async function ensureRepairDataset({ source, localDb, indexerDb, client,
36
45
  const existingDatasetId = await findRepairDataset({
37
46
  indexerDb,
38
47
  providerId: repair.targetProviderId,
39
- payer: client.account.address,
48
+ payer,
40
49
  source,
41
50
  })
42
51
 
@@ -47,7 +56,7 @@ export async function ensureRepairDataset({ source, localDb, indexerDb, client,
47
56
  const { txHash, statusUrl } = await SP.createDataSet(client, {
48
57
  payee: provider.payee,
49
58
  serviceURL: provider.pdp.serviceURL,
50
- payer: client.account.address,
59
+ payer,
51
60
  cdn: false,
52
61
  metadata: {
53
62
  source,
@@ -74,7 +83,13 @@ export async function ensureRepairDataset({ source, localDb, indexerDb, client,
74
83
  *
75
84
  * @param options - The options for ensuring the replication dataset.
76
85
  */
77
- export async function ensureReplicateDataset({ localDb, indexerDb, client, repair }: EnsureRepairDatasetOptions) {
86
+ export async function ensureReplicateDataset({
87
+ localDb,
88
+ indexerDb,
89
+ client,
90
+ repair,
91
+ payer,
92
+ }: EnsureRepairDatasetOptions) {
78
93
  const log = p.taskLog({
79
94
  title: 'Ensuring replication dataset',
80
95
  })
@@ -110,7 +125,7 @@ export async function ensureReplicateDataset({ localDb, indexerDb, client, repai
110
125
  const { txHash, statusUrl } = await SP.createDataSet(client, {
111
126
  payee: provider.payee,
112
127
  serviceURL: provider.pdp.serviceURL,
113
- payer: client.account.address,
128
+ payer,
114
129
  cdn: sourceDataSet.withCdn,
115
130
  metadata: sourceDataSet.metadata ?? undefined,
116
131
  })
package/src/utils.ts CHANGED
@@ -1,8 +1,7 @@
1
- import type { MetadataObject } from '@filoz/synapse-core'
2
1
  import { type Chain, getChain } from '@filoz/synapse-core/chains'
3
2
  import * as Piece from '@filoz/synapse-core/piece'
4
3
  import type * as SP from '@filoz/synapse-core/sp'
5
- import { getTableColumns, type SQL, sql } from 'drizzle-orm'
4
+ import { getTableColumns, inArray, type SQL, sql } from 'drizzle-orm'
6
5
  import { drizzle } from 'drizzle-orm/libsql'
7
6
  import type { PgTable } from 'drizzle-orm/pg-core'
8
7
  import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
@@ -11,12 +10,13 @@ import { Conf } from 'iso-conf'
11
10
  import { request } from 'iso-web/http'
12
11
  import pLocate from 'p-locate'
13
12
  import terminalLink from 'terminal-link'
14
- import { createWalletClient, type Hex, http } from 'viem'
13
+ import { createWalletClient, type Hash, type Hex, http, TransactionReceiptNotFoundError } from 'viem'
15
14
  import { privateKeyToAccount } from 'viem/accounts'
15
+ import { getTransactionReceipt } from 'viem/actions'
16
16
  import packageJson from '../package.json' with { type: 'json' }
17
17
  import type { OperationSelect } from './local-schema.ts'
18
18
  import * as schema from './local-schema.ts'
19
- import type { LocalDatabase } from './types.ts'
19
+ import type { LocalDatabase, WalletClient } from './types.ts'
20
20
 
21
21
  export const configSchema = z.object({
22
22
  privateKey: z.string().optional(),
@@ -81,6 +81,7 @@ export async function createLocalDatabase(dbPath: string): Promise<LocalDatabase
81
81
  }) as LocalDatabase
82
82
 
83
83
  await localDb.$client.execute('PRAGMA journal_mode = WAL')
84
+ await migrateLocalDatabase(localDb)
84
85
 
85
86
  return localDb
86
87
  }
@@ -109,6 +110,25 @@ export async function migrateLocalDatabase(db: LocalDatabase) {
109
110
  } catch {
110
111
  // Column already exists on databases created with the updated schema.
111
112
  }
113
+ try {
114
+ await db.$client.execute(`
115
+ ALTER TABLE repairs ADD COLUMN kind text NOT NULL DEFAULT 'repair'
116
+ `)
117
+ } catch {
118
+ // Column already exists on databases created with the updated schema.
119
+ }
120
+ try {
121
+ await db.$client.execute(`
122
+ ALTER TABLE repairs ADD COLUMN source_provider_url text
123
+ `)
124
+ } catch {
125
+ // Column already exists on databases created with the updated schema.
126
+ }
127
+ await db.$client.execute(`
128
+ UPDATE repairs
129
+ SET kind = 'replicate'
130
+ WHERE repair_data_set_id IS NOT NULL AND (kind IS NULL OR kind = 'repair')
131
+ `)
112
132
  await db.$client.execute(`
113
133
  CREATE TABLE IF NOT EXISTS operations (
114
134
  id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
@@ -118,13 +138,27 @@ export async function migrateLocalDatabase(db: LocalDatabase) {
118
138
  cid text NOT NULL,
119
139
  metadata text NOT NULL,
120
140
  alternate_provider text NOT NULL,
121
- result text,
141
+ tx_hash text,
122
142
  error text,
123
143
  created_at integer NOT NULL,
124
144
  updated_at integer NOT NULL,
125
145
  FOREIGN KEY (repair_id) REFERENCES repairs(id)
126
146
  )
127
147
  `)
148
+ try {
149
+ await db.$client.execute(`
150
+ ALTER TABLE operations ADD COLUMN tx_hash text
151
+ `)
152
+ } catch {
153
+ // Column already exists on databases created with the updated schema.
154
+ }
155
+ try {
156
+ await db.$client.execute(`
157
+ ALTER TABLE operations DROP COLUMN result
158
+ `)
159
+ } catch {
160
+ // Column does not exist on databases created with the updated schema.
161
+ }
128
162
  }
129
163
 
130
164
  /**
@@ -139,6 +173,62 @@ export function hashLink(hash: string, chain: Chain) {
139
173
  return link
140
174
  }
141
175
 
176
+ /**
177
+ * Mark operations with successful mined transactions as completed.
178
+ *
179
+ * @returns Operations that are not completed yet.
180
+ */
181
+ export async function completeConfirmedOperations({
182
+ localDb,
183
+ client,
184
+ operations,
185
+ }: {
186
+ localDb: LocalDatabase
187
+ client: WalletClient
188
+ operations: OperationSelect[]
189
+ }): Promise<OperationSelect[]> {
190
+ const hashes = new Set(
191
+ operations.map((operation) => operation.txHash).filter((txHash): txHash is Hash => txHash != null)
192
+ )
193
+ const completedHashes = new Set<Hash>()
194
+
195
+ for (const hash of hashes) {
196
+ try {
197
+ const receipt = await getTransactionReceipt(client, { hash })
198
+ if (receipt.status === 'success') {
199
+ completedHashes.add(hash)
200
+ }
201
+ } catch (error) {
202
+ if (!(error instanceof TransactionReceiptNotFoundError)) {
203
+ throw error
204
+ }
205
+ }
206
+ }
207
+
208
+ const completedOperations = operations.filter(
209
+ (operation) => operation.txHash != null && completedHashes.has(operation.txHash as Hash)
210
+ )
211
+
212
+ if (completedOperations.length > 0) {
213
+ await localDb
214
+ .update(schema.operations)
215
+ .set({
216
+ status: 'completed',
217
+ error: null,
218
+ updatedAt: Date.now(),
219
+ })
220
+ .where(
221
+ inArray(
222
+ schema.operations.id,
223
+ completedOperations.map((operation) => operation.id)
224
+ )
225
+ )
226
+ }
227
+ const completedOperationIds = new Set(completedOperations.map((operation) => operation.id))
228
+
229
+ return operations.filter((operation) => operation.status !== 'completed' && !completedOperationIds.has(operation.id))
230
+ }
231
+
142
232
  /**
143
233
  * Get a piece from a service URL
144
234
  */