Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
"types": "./dist/core/payments/index.d.ts",
"default": "./dist/core/payments/index.js"
},
"./core/piece": {
"types": "./dist/core/piece/index.d.ts",
"default": "./dist/core/piece/index.js"
},
"./core/synapse": {
"types": "./dist/core/synapse/index.d.ts",
"default": "./dist/core/synapse/index.js"
Expand Down
2 changes: 2 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { addCommand } from './commands/add.js'
import { dataSetCommand } from './commands/data-set.js'
import { importCommand } from './commands/import.js'
import { paymentsCommand } from './commands/payments.js'
import { rmCommand } from './commands/rm.js'
import { serverCommand } from './commands/server.js'
import { checkForUpdate, type UpdateCheckStatus } from './common/version-check.js'
import { version as packageVersion } from './core/utils/version.js'
Expand All @@ -24,6 +25,7 @@ program.addCommand(paymentsCommand)
program.addCommand(dataSetCommand)
program.addCommand(importCommand)
program.addCommand(addCommand)
program.addCommand(rmCommand)

// Default action - show help if no command specified
program.action(() => {
Expand Down
19 changes: 19 additions & 0 deletions src/commands/rm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Command } from 'commander'
import { runRmPiece } from '../rm/index.js'
import { addAuthOptions } from '../utils/cli-options.js'

export const rmCommand = new Command('rm')
.description('Remove a Piece from a DataSet')
.option('--piece <cid>', 'Piece CID to remove')
.option('--data-set <id>', 'DataSet ID to remove the piece from')
.option('--wait-for-confirmation', 'Wait for transaction confirmation before exiting')
.action(async (options) => {
try {
await runRmPiece(options)
} catch {
// Error already displayed by clack UI in runRmPiece
process.exit(1)
}
})

addAuthOptions(rmCommand)
1 change: 1 addition & 0 deletions src/core/piece/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './remove-piece.js'
215 changes: 215 additions & 0 deletions src/core/piece/remove-piece.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/**
* Remove piece functionality
*
* This module demonstrates the pattern for removing pieces from Data Sets
* via Synapse SDK. It supports two usage patterns:
*
* 1. With dataSetId - creates a temporary storage context (CLI usage)
* 2. With existing StorageContext - reuses context (library/server usage)
*
* Progress events allow callers to track transaction submission and confirmation.
*/
import type { StorageContext, Synapse } from '@filoz/synapse-sdk'
import type { Logger } from 'pino'
import { createStorageContext } from '../synapse/index.js'
import { getErrorMessage } from '../utils/errors.js'
import type { ProgressEvent, ProgressEventHandler } from '../utils/types.js'

/**
* Progress events emitted during piece removal
*
* These events allow callers to track the removal process:
* - submitting: Transaction is being submitted to blockchain
* - submitted: Transaction submitted successfully, txHash available
* - confirming: Waiting for transaction confirmation (if waitForConfirmation=true)
* - confirmation-failed: Confirmation wait timed out (non-fatal, tx may still succeed)
* - complete: Removal process finished
*
* Note: Errors are propagated via thrown exceptions, not events (similar to upload pattern)
*/
export type RemovePieceProgressEvents =
| ProgressEvent<'remove-piece:submitting', { pieceCid: string; dataSetId: number }>
| ProgressEvent<'remove-piece:submitted', { pieceCid: string; dataSetId: number; txHash: `0x${string}` | string }>
| ProgressEvent<'remove-piece:confirming', { pieceCid: string; dataSetId: number; txHash: `0x${string}` | string }>
| ProgressEvent<
'remove-piece:confirmation-failed',
{ pieceCid: string; dataSetId: number; txHash: `0x${string}` | string; message: string }
>
| ProgressEvent<'remove-piece:complete', { txHash: `0x${string}` | string; confirmed: boolean }>

/**
* Number of block confirmations to wait for when waitForConfirmation=true
*/
const WAIT_CONFIRMATIONS = 1

/**
* Timeout in milliseconds for waiting for transaction confirmation
* Set to 2 minutes - generous default for Calibration network finality
*/
const WAIT_TIMEOUT_MS = 2 * 60 * 1000

/**
* Base options for piece removal
*/
interface RemovePieceOptionsBase {
/** Initialized Synapse SDK instance */
synapse: Synapse
/** Optional progress event handler for tracking removal status */
onProgress?: ProgressEventHandler<RemovePieceProgressEvents> | undefined
/** Whether to wait for transaction confirmation before returning (default: false) */
waitForConfirmation?: boolean | undefined
/** Optional logger for tracking removal operations */
logger?: Logger | undefined
}

/**
* Options for removing a piece when you have a dataSetId
*
* This is the typical CLI usage pattern - you know the dataSetId and want
* to remove a piece from it. A temporary storage context will be created.
*
* Note: logger is required in this mode for storage context creation.
*/
interface RemovePieceOptionsWithDataSetId extends RemovePieceOptionsBase {
/** The Data Set ID containing the piece to remove */
dataSetId: number
/**
* Logger instance (required for storage context creation)
* @see https://github.com/filecoin-project/filecoin-pin/issues/252
*/
logger: Logger
}

/**
* Options for removing a piece when you have an existing StorageContext
*
* This is useful for library/server usage where you already have a storage
* context and want to remove multiple pieces without recreating the context.
*/
interface RemovePieceOptionsWithStorage extends RemovePieceOptionsBase {
/** Existing storage context bound to a Data Set */
storage: StorageContext
}

/**
* Options for removing a piece from a Data Set
*
* Supports two patterns:
* - With dataSetId: Creates temporary storage context (CLI pattern)
* - With storage: Reuses existing context (library/server pattern)
*/
export type RemovePieceOptions = RemovePieceOptionsWithDataSetId | RemovePieceOptionsWithStorage

/**
* Remove a piece from a Data Set
*
* This function demonstrates the pattern for removing pieces via Synapse SDK.
* It supports two usage patterns:
*
* Pattern 1 - With dataSetId (typical CLI usage):
* ```typescript
* const txHash = await removePiece('baga...', {
* synapse,
* dataSetId: 42,
* logger,
* onProgress: (event) => console.log(event.type),
* waitForConfirmation: true
* })
* ```
*
* Pattern 2 - With existing StorageContext (library/server usage):
* ```typescript
* const { storage } = await createStorageContext(synapse, logger, {...})
* const txHash = await removePiece('baga...', {
* synapse,
* storage,
* onProgress: (event) => console.log(event.type)
* })
* ```
*
* @param pieceCid - The Piece CID to remove from the Data Set
* @param options - Configuration options (dataSetId or storage context)
* @returns Transaction hash of the removal operation
* @throws Error if storage context is not bound to a Data Set
*/
export async function removePiece(pieceCid: string, options: RemovePieceOptions): Promise<`0x${string}` | string> {
// Check dataSetId first
if (isRemovePieceOptionsWithDataSetId(options)) {
const { dataSetId, logger, synapse } = options
const { storage } = await createStorageContext(synapse, logger, { dataset: { useExisting: dataSetId } })
return executeRemovePiece(pieceCid, dataSetId, storage, options)
}

// Handle existing storage context (library/server usage)
if (isRemovePieceOptionsWithStorage(options)) {
const dataSetId = options.storage.dataSetId
if (dataSetId == null) {
throw new Error(
'Storage context must be bound to a Data Set before removing pieces. Use createStorageContext with dataset.useExisting to bind to a Data Set.'
)
}
return executeRemovePiece(pieceCid, dataSetId, options.storage, options)
}

// Should never get here, but we need some clear error message if we do.
throw new Error('Invalid options: must provide either dataSetId or storage context')
}

/**
* Type guard to check if options include dataSetId
*/
function isRemovePieceOptionsWithDataSetId(options: RemovePieceOptions): options is RemovePieceOptionsWithDataSetId {
return 'dataSetId' in options
}

/**
* Type guard to check if options include storage context
*/
function isRemovePieceOptionsWithStorage(options: RemovePieceOptions): options is RemovePieceOptionsWithStorage {
return 'storage' in options && options.storage != null
}

/**
* Execute the piece removal operation
*
* This internal function handles the actual removal:
* 1. Submits transaction via storageContext.deletePiece()
* 2. Optionally waits for confirmation if requested
* 3. Emits progress events throughout the process
*
* @param pieceCid - The Piece CID to remove
* @param dataSetId - The Data Set ID (for progress events)
* @param storageContext - Storage context bound to the Data Set
* @param options - Base options including callbacks and confirmation settings
* @returns Transaction hash of the removal
*/
async function executeRemovePiece(
pieceCid: string,
dataSetId: number,
storageContext: StorageContext,
options: RemovePieceOptionsBase
): Promise<`0x${string}` | string> {
const { onProgress, synapse, waitForConfirmation } = options

onProgress?.({ type: 'remove-piece:submitting', data: { pieceCid, dataSetId } })
const txHash = await storageContext.deletePiece(pieceCid)
onProgress?.({ type: 'remove-piece:submitted', data: { pieceCid, dataSetId, txHash } })

let isConfirmed = false
if (waitForConfirmation === true) {
onProgress?.({ type: 'remove-piece:confirming', data: { pieceCid, dataSetId, txHash } })
try {
await synapse.getProvider().waitForTransaction(txHash, WAIT_CONFIRMATIONS, WAIT_TIMEOUT_MS)
isConfirmed = true
} catch (error: unknown) {
// Confirmation timeout is non-fatal - transaction may still succeed
onProgress?.({
type: 'remove-piece:confirmation-failed',
data: { pieceCid, dataSetId, txHash, message: getErrorMessage(error) as string },
})
}
}

onProgress?.({ type: 'remove-piece:complete', data: { txHash, confirmed: isConfirmed } })
return txHash
}
1 change: 1 addition & 0 deletions src/rm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './remove-piece.js'
Loading
Loading