Skip to content

Commit b3439ad

Browse files
authored
refactor(storage): split immutable context targets (#2)
* refactor(storage): split immutable context targets * style(signer): satisfy gofumpt * refactor(storage): make context APIs explicit * fix(storage): restore identity, resume, and context validation * fix(storage): validate replacements and restore coverage gates
1 parent c99b724 commit b3439ad

45 files changed

Lines changed: 3824 additions & 6282 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ build:
1616
test:
1717
go test ./...
1818

19-
# Run benchmarks — auto-discovers packages containing *_bench_test.go files
19+
# Run benchmarks — auto-discovers packages containing *_bench_test.go files.
20+
# Skip hidden dirs (worktrees, .git) and local reference checkouts.
2021
bench:
21-
go test -bench=. -benchmem $(shell find . -name '*_bench_test.go' | sed 's|/[^/]*$$||' | sort -u)
22+
go test -bench=. -benchmem $(shell find . \( -path '*/.*' -o -path './lotus' -o -path './curio' -o -path './synapse-sdk' -o -path './go-synapse' \) -prune -o -name '*_bench_test.go' -print | sed 's|/[^/]*$$||' | sort -u)
2223

2324
# Run tests with race detector
2425
test-race:

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
[![License](https://img.shields.io/github/license/strahe/synapse-go)](LICENSE)
88
[![Go Version](https://img.shields.io/badge/go-1.26.3%2B-00ADD8)](go.mod)
99

10-
Go SDK for Filecoin Onchain Cloud (FOC), ported from the
11-
[@filoz/synapse-sdk](https://github.com/FilOzone/synapse-sdk).
10+
Go SDK for Filecoin Onchain Cloud (FOC).
1211

1312
> **Status:** Beta - API may change.
1413

doc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
//
1818
// // data must contain uploadable content; PieceCIDv2 requires at least
1919
// // 127 raw bytes.
20-
// result, err := client.Storage().Upload(ctx, data, nil)
20+
// result, err := client.Storage().Upload(ctx, data, &storage.UploadOptions{Copies: 2})
2121
//
2222
// Sub-services are accessed via getters: [Client.Storage], [Client.Payments],
2323
// [Client.WarmStorage], [Client.SPRegistry], [Client.Costs], [Client.FilBeam],
@@ -34,7 +34,7 @@
3434
// This SDK is in its 0.x phase. Public APIs may change between minor
3535
// releases; breaking changes are called out in release notes. Pin to a
3636
// specific minor version in production. The implementation tracks the
37-
// Filecoin Onchain Cloud protocol and the upstream TypeScript SDK.
37+
// Filecoin Onchain Cloud protocol.
3838
//
3939
// [piece]: https://pkg.go.dev/github.com/strahe/synapse-go/piece
4040
package synapse

docs/GETTING_STARTED.md

Lines changed: 120 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,9 @@ exceeding it returns `storage.ErrMaxBytesExceeded`.
110110

111111
## Upload Controls
112112

113-
Use `storage.UploadOptions` when the default upload is not enough:
113+
`Service.Upload` performs automatic target selection. Its options include:
114114

115-
- `Copies`: requested provider copies. Zero means the selection default.
116-
- `ProviderIDs`: pin copies to specific providers.
117-
- `DataSetIDs`: write to specific existing datasets.
115+
- `Copies`: required number of provider copies. It must be greater than zero.
118116
- `ExcludeProviderIDs`: skip providers only during automatic selection.
119117
- `DataSetMetadata`: metadata used when creating or reusing datasets.
120118
- `PieceMetadata`: metadata stored with the committed piece.
@@ -133,20 +131,40 @@ to know whether every requested copy succeeded.
133131
Dataset metadata must match exactly for automatic dataset reuse. Use stable
134132
metadata values when you want uploads to share payment rails.
135133

134+
Use `NewProviderContext` or `NewDataSetContext` for a known target. Use
135+
`UploadToContexts` when the caller, rather than the SDK, must determine the
136+
exact providers and their primary-to-secondary order.
137+
136138
## Funding Preflight
137139

138140
`Prepare` is optional. Use it before a first upload, before a large batch, or
139141
when your UI needs to show whether the account has enough USDFC deposit and
140-
FWSS approval. If `Transaction` is nil, the account is already ready for the
141-
requested size and contexts. Match options that affect context selection, such
142-
as CDN.
142+
FWSS approval. Select the upload targets first, then pass the same contexts to
143+
`Prepare` and `UploadToContexts`. This ensures the estimate and upload use the
144+
same providers, datasets, payer, chain, and record keeper.
143145

144146
```go
145147
withCDN := true
146148

149+
selection, selectErr := client.Storage().SelectUploadContexts(ctx,
150+
storage.SelectUploadContextsOptions{
151+
Copies: 2,
152+
WithCDN: &withCDN,
153+
DataSetMetadata: map[string]string{
154+
"project": "photos",
155+
},
156+
},
157+
)
158+
if selectErr != nil && !errors.Is(selectErr, storage.ErrInsufficientUploadContexts) {
159+
return selectErr
160+
}
161+
if selection == nil {
162+
return errors.New("no upload contexts available")
163+
}
164+
147165
prep, err := client.Storage().Prepare(ctx, &storage.PrepareOptions{
148-
DataSize: uint64(payloadSize),
149-
EnableCDN: &withCDN,
166+
DataSize: uint64(payloadSize),
167+
Contexts: selection.Contexts,
150168
})
151169
if err != nil {
152170
return err
@@ -159,62 +177,91 @@ if prep.Transaction != nil {
159177
}
160178
fmt.Println("prepare tx:", tx.Hash)
161179
}
180+
181+
result, err := client.Storage().UploadToContexts(
182+
ctx,
183+
file,
184+
selection.Contexts,
185+
&storage.UploadOptions{
186+
PieceMetadata: map[string]string{"name": "payload.bin"},
187+
},
188+
)
189+
if err != nil {
190+
return err
191+
}
162192
```
163193

164-
If you already selected contexts, pass them through `PrepareOptions.Contexts`
165-
so the estimate matches the exact providers and datasets. `CreateContexts`
166-
returns managed `*storage.Context` values; convert the slice element-by-element
167-
when filling the `[]storage.UploadContext` field.
194+
When selection finds at least one but fewer than the requested targets, it
195+
returns both a usable `UploadContextSelection` and an
196+
`InsufficientUploadContextsError`. The application can continue with the
197+
available contexts or stop before funding. With `UploadToContexts`, the
198+
selection length becomes `UploadResult.RequestedCopies` and no replacement
199+
provider is selected automatically.
168200

169201
For read-only cost and account state, use `GetStorageInfo` or
170202
`CalculateMultiContextCosts`.
171203

172204
## Contexts And Datasets
173205

174-
Use contexts when you need provider or dataset control before uploading.
206+
There are two immutable context types:
207+
208+
- `ProviderContext` identifies one provider and no dataset. `Commit` and
209+
`Pull` create a new dataset.
210+
- `DataSetContext` identifies one provider and one existing dataset. `Commit`
211+
and `Pull` always target that dataset.
212+
213+
Provider-scoped methods such as `Store` and `Download` are shared. For example,
214+
both `ProviderContext.Download` and `DataSetContext.Download` retrieve a piece
215+
from the same configured provider or CDN; the dataset binding does not change
216+
piece retrieval. Dataset inspection, deletion, and termination methods exist
217+
only on `DataSetContext`.
218+
219+
Select one approved, active, healthy provider without looking up datasets:
175220

176221
```go
177-
contexts, err := client.Storage().CreateContexts(ctx, &storage.CreateContextsOptions{
178-
Copies: 2,
179-
DataSetMetadata: map[string]string{
180-
"project": "photos",
222+
providerCtx, err := client.Storage().SelectProviderContext(ctx,
223+
storage.SelectProviderContextOptions{
224+
DataSetMetadata: map[string]string{
225+
"project": "photos",
226+
},
181227
},
182-
})
228+
)
183229
if err != nil {
184230
return err
185231
}
186-
187-
fmt.Println("contexts:", len(contexts))
188232
```
189233

190-
```go
191-
prepareContexts := make([]storage.UploadContext, len(contexts))
192-
for i, c := range contexts {
193-
prepareContexts[i] = c
194-
}
234+
Open a registered provider by ID without checking approval, activity, endpoint
235+
health, or existing datasets:
195236

196-
prep, err := client.Storage().Prepare(ctx, &storage.PrepareOptions{
197-
DataSize: uint64(payloadSize),
198-
Contexts: prepareContexts,
199-
})
237+
```go
238+
providerID := types.NewBigInt(123)
239+
providerCtx, err := client.Storage().NewProviderContext(ctx, providerID,
240+
storage.NewProviderContextOptions{
241+
DataSetMetadata: map[string]string{"project": "photos"},
242+
},
243+
)
200244
if err != nil {
201245
return err
202246
}
203-
fmt.Println("ready:", prep.Costs.Ready)
204247
```
205248

206-
For one provider or one dataset:
249+
Open an existing dataset owned by the current payer. The optional provider ID
250+
is an ownership assertion. Opening a terminated or currently unwritable
251+
dataset is allowed for inspection and cleanup; a later `Commit` or `Upload`
252+
still checks writability.
207253

208254
```go
209255
providerID := types.NewBigInt(123)
210-
ctx1, err := client.Storage().CreateContext(ctx, &storage.CreateContextOptions{
211-
ProviderID: &providerID,
212-
})
256+
dataSetID := types.NewBigInt(456)
257+
dataSetCtx, err := client.Storage().NewDataSetContext(ctx, dataSetID,
258+
storage.NewDataSetContextOptions{ProviderID: &providerID},
259+
)
213260
if err != nil {
214261
return err
215262
}
216263

217-
result, err := ctx1.Upload(ctx, file, &storage.UploadOptions{
264+
result, err := dataSetCtx.Upload(ctx, file, &storage.UploadOptions{
218265
PieceMetadata: map[string]string{"name": "payload.bin"},
219266
})
220267
if err != nil {
@@ -223,65 +270,80 @@ if err != nil {
223270
fmt.Println(result.PieceCID)
224271
```
225272

226-
When resuming a known dataset, pass `DataSetID`. If you also pass
227-
`ProviderID`, the SDK checks that the dataset belongs to that provider.
273+
`DataSetRef` is the persistent reference for a complete provider and dataset
274+
target. Its zero value is invalid; construct it explicitly and use accessors to
275+
read IDs.
228276

229277
```go
230-
dataSetID := types.NewBigInt(456)
231-
providerID := types.NewBigInt(123)
232-
ctx1, err := client.Storage().CreateContext(ctx, &storage.CreateContextOptions{
233-
DataSetID: &dataSetID,
234-
ProviderID: &providerID,
235-
})
278+
ref, err := storage.NewDataSetRef(providerID, dataSetID, dataSetCtx.ClientDataSetID())
236279
if err != nil {
237280
return err
238281
}
282+
fmt.Println("dataset:", ref.DataSetID())
239283
```
240284

241-
To create an empty dataset first, persist the submission if your process may
242-
restart before confirmation:
285+
To create an empty dataset first, persist the submission if the process may
286+
restart before confirmation. Creation is available only on `ProviderContext`.
243287

244288
```go
245289
var submitted storage.CreateDataSetSubmission
246290

247-
created, err := ctx1.CreateDataSet(ctx, &storage.CreateDataSetOptions{
291+
created, err := providerCtx.CreateDataSet(ctx, &storage.CreateDataSetOptions{
248292
OnSubmitted: func(s storage.CreateDataSetSubmission) {
249293
submitted = s
250294
},
251295
})
252296
if err != nil {
253297
return err
254298
}
255-
fmt.Println("dataset:", created.DataSetID)
299+
fmt.Println("dataset:", created.DataSet.DataSetID())
256300
```
257301

258-
Resume a submitted create transaction:
302+
Resume a submitted create transaction with any fresh `ProviderContext` for the
303+
same provider, then convert the returned reference without mutating that
304+
context:
259305

260306
```go
261-
created, err := ctx1.WaitForDataSetCreated(ctx, submitted)
307+
created, err := providerCtx.WaitForDataSetCreated(ctx, submitted)
262308
if err != nil {
263309
return err
264310
}
265-
fmt.Println("dataset:", created.DataSetID)
311+
dataSetCtx, err := providerCtx.ForDataSet(created.DataSet)
312+
if err != nil {
313+
return err
314+
}
315+
fmt.Println("dataset:", dataSetCtx.DataSetID())
266316
```
267317

268-
Use `GetDefaultContext` when the context resolver defaults are enough.
269-
Advanced callers can split a context upload into `Store`, `Pull`,
318+
The receiver never binds or changes target after creation. Concurrent creates
319+
on one `ProviderContext` are independent; adds on one `DataSetContext` may run
320+
in parallel. Advanced callers can split a context upload into `Store`, `Pull`,
270321
`PresignForCommit`, and `Commit`.
271322

323+
### Migrating From The Previous Context API
324+
325+
| Previous call | Replacement |
326+
|---------------|-------------|
327+
| `CreateContext(nil)` / `GetDefaultContext()` | `SelectProviderContext(...)` |
328+
| `CreateContext` with `ProviderID` | `NewProviderContext(...)` |
329+
| `CreateContext` with `DataSetID` | `NewDataSetContext(...)` |
330+
| `CreateContexts` for a new upload | `SelectUploadContexts(...)` |
331+
| `Upload` with provider or dataset IDs | construct/select contexts, then call `UploadToContexts(...)` |
332+
| `Prepare` without contexts | select contexts first and pass the same slice to `Prepare` |
333+
272334
## Discovery And Lifecycle
273335

274336
Common management calls:
275337

276338
- `FindDataSets`: list datasets owned by the signer or another payer.
277339
- `GetStorageInfo`: inspect providers, pricing, limits, and allowances.
278-
- `Context.Download`: download from a known provider and dataset context.
279-
- `Context.DeletePieceByID`: schedule exact removal by on-chain piece ID.
280-
- `Context.DeletePiece`: schedule removal by piece CID convenience lookup. Prefer
340+
- `ProviderContext.Download` / `DataSetContext.Download`: download from a known provider.
341+
- `DataSetContext.DeletePieceByID`: schedule exact removal by on-chain piece ID.
342+
- `DataSetContext.DeletePiece`: schedule removal by piece CID convenience lookup. Prefer
281343
`DeletePieceByID` when available, because repeated uploads can share a CID.
282-
- `Context.TerminateService` / `Service.TerminateService`: terminate service
344+
- `DataSetContext.TerminateService` / `Service.TerminateService`: terminate service
283345
through the provider by default; use `SkipProvider` for direct FWSS fallback.
284-
- `Context.Terminate` / `Service.TerminateDataSet`: legacy direct FWSS
346+
- `DataSetContext.Terminate` / `Service.TerminateDataSet`: legacy direct FWSS
285347
termination write.
286348

287349
Termination and removal are storage lifecycle actions. Treat them as

examples/quickstart/main.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
)
2929

3030
const (
31+
quickstartCopies = 2
3132
quickstartSource = "synapse-go-quickstart"
3233
quickstartDownloadAttempts = 5
3334
quickstartDownloadRetryDelay = 30 * time.Second
@@ -117,7 +118,18 @@ func runQuickstart(ctx context.Context, cfg quickstartConfig, svc quickstartStor
117118
return err
118119
}
119120

120-
prepare, err := svc.Prepare(ctx, &storage.PrepareOptions{DataSize: uint64(len(cfg.Payload))})
121+
selection, err := svc.SelectUploadContexts(ctx, storage.SelectUploadContextsOptions{Copies: quickstartCopies})
122+
if err != nil && !errors.Is(err, storage.ErrInsufficientUploadContexts) {
123+
return fmt.Errorf("select upload contexts: %w", err)
124+
}
125+
if selection == nil {
126+
return errors.New("select upload contexts: no selection returned")
127+
}
128+
129+
prepare, err := svc.Prepare(ctx, &storage.PrepareOptions{
130+
DataSize: uint64(len(cfg.Payload)),
131+
Contexts: selection.Contexts,
132+
})
121133
if err != nil {
122134
return fmt.Errorf("prepare upload: %w", err)
123135
}
@@ -134,7 +146,7 @@ func runQuickstart(ctx context.Context, cfg quickstartConfig, svc quickstartStor
134146
}
135147
}
136148

137-
upload, err := svc.Upload(ctx, bytes.NewReader(cfg.Payload), nil)
149+
upload, err := svc.UploadToContexts(ctx, bytes.NewReader(cfg.Payload), selection.Contexts, nil)
138150
if err != nil {
139151
return fmt.Errorf("upload: %w", err)
140152
}
@@ -156,8 +168,9 @@ func runQuickstart(ctx context.Context, cfg quickstartConfig, svc quickstartStor
156168
}
157169

158170
type quickstartStorage interface {
171+
SelectUploadContexts(context.Context, storage.SelectUploadContextsOptions) (*storage.UploadContextSelection, error)
159172
Prepare(context.Context, *storage.PrepareOptions) (*storage.PrepareResult, error)
160-
Upload(context.Context, io.Reader, *storage.UploadOptions) (*storage.UploadResult, error)
173+
UploadToContexts(context.Context, io.Reader, []storage.StorageContext, *storage.UploadOptions) (*storage.UploadResult, error)
161174
Download(context.Context, cid.Cid, *storage.DownloadOptions) (io.ReadCloser, error)
162175
}
163176

@@ -169,8 +182,12 @@ func (w storageWorkflow) Prepare(ctx context.Context, opts *storage.PrepareOptio
169182
return w.svc.Prepare(ctx, opts)
170183
}
171184

172-
func (w storageWorkflow) Upload(ctx context.Context, r io.Reader, opts *storage.UploadOptions) (*storage.UploadResult, error) {
173-
return w.svc.Upload(ctx, r, opts)
185+
func (w storageWorkflow) SelectUploadContexts(ctx context.Context, opts storage.SelectUploadContextsOptions) (*storage.UploadContextSelection, error) {
186+
return w.svc.SelectUploadContexts(ctx, opts)
187+
}
188+
189+
func (w storageWorkflow) UploadToContexts(ctx context.Context, r io.Reader, contexts []storage.StorageContext, opts *storage.UploadOptions) (*storage.UploadResult, error) {
190+
return w.svc.UploadToContexts(ctx, r, contexts, opts)
174191
}
175192

176193
func (w storageWorkflow) Download(ctx context.Context, pieceCID cid.Cid, opts *storage.DownloadOptions) (io.ReadCloser, error) {

0 commit comments

Comments
 (0)