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
41 changes: 41 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"permissions": {
"allow": [
"Bash(gh issue *)",
"WebFetch(domain:github.com)",
"Bash(curl -s \"https://api.github.com/repos/FilOzone/synapse-sdk/issues/695/comments\")",
"Bash(grep -E \"\\\\.ts$\")",
"Bash(find /Users/julian/dev/filozofone/synapse-sdk/packages/synapse-core -name \"README.md\")",
"Bash(pnpm run *)",
"Bash(which corepack *)",
"Bash(corepack enable *)",
"Bash(pnpm install *)",
"Bash(pnpm test *)",
"Bash(WIREIT_FAILURES=continue WIREIT_LOGGER=quiet npx playwright-test \"src/test/session-keys.test.ts\" --mode node)",
"Bash(npx playwright-test *)",
"Bash(git add *)",
"Bash(git push *)",
"Bash(gh --version)",
"Bash(env)",
"Read(//Users/julian/.config/gh/**)",
"Bash(brew list *)",
"Bash(find /opt/homebrew /usr/local -name \"gh\" -type f 2>/dev/null | head -5; ls /Applications/GitHub*.app 2>/dev/null | head -3)",
"Bash(/opt/homebrew/Cellar/gh/2.92.0/bin/gh auth *)",
"Bash(git checkout *)",
"Bash(git commit -m ' *)",
"Bash(git fetch *)",
"Bash(git pull *)",
"Read(//Users/julian/dev/filozone/**)",
"Read(//Users/julian/dev/**)",
"Bash(gh pr *)",
"Bash(gh api *)",
"Read(//tmp/**)",
"Bash(tar xzf *)",
"Bash(cd /tmp/isoweb3/package && echo \"=== exports/files ===\" && ls src/ && echo \"=== http.js line count ===\" && wc -l src/http.js)",
"Bash(xargs cat)",
"Bash(npm view *)",
"Bash(npm pack *)",
"Bash(echo \"=== publish times ===\" && npm view iso-web time --json 2>/dev/null | grep -E '\"3\\\\.\\(0|1\\)\\\\.' ; echo \"today: 2026-06-03\"; echo \"=== shouldRetry + status throw logic in 3.1.1 ===\" && grep -n 'shouldRetry\\\\|isRetryStatus\\\\|isNetworkError\\\\|statusCodes.includes\\\\|Boolean\\(result\\)\\\\|!rsp.ok' /tmp/iso2/package/src/http.js | head -30)"
]
}
}
6 changes: 5 additions & 1 deletion packages/synapse-core/src/utils/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@ export function datasetMetadataObjectToEntry(
metadataObject?: MetadataObject,
metadataInternal?: MetadataDataSetInternal
): MetadataEntry[] {
// When the cdn flag is set, ensure the withCDN key is present. Preserve any value already on the
// incoming metadata (the CDN group id that FWSS keys the shared bandwidth rail by); only default
// to an empty value when the key is absent, so the group id is not clobbered.
const ensureWithCDN = metadataInternal?.cdn === true && (metadataObject == null || !('withCDN' in metadataObject))
const obj = {
...(metadataObject ?? {}),
...(metadataInternal?.cdn ? { withCDN: '' } : {}),
...(ensureWithCDN ? { withCDN: '' } : {}),
}
const entries = Object.entries(obj)
.sort((a, b) => a[0].localeCompare(b[0]))
Expand Down
9 changes: 9 additions & 0 deletions packages/synapse-core/test/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ describe('Metadata Utils', () => {
])
})

it('should preserve an existing non-empty withCDN value (CDN group id) when cdn flag is true', () => {
const result = datasetMetadataObjectToEntry({ project: 'test', withCDN: '0xpayer' }, { cdn: true })

assert.deepStrictEqual(result, [
{ key: 'project', value: 'test' },
{ key: 'withCDN', value: '0xpayer' },
])
})

it('should not add withCDN when cdn internal flag is false', () => {
const result = datasetMetadataObjectToEntry({ project: 'test' }, { cdn: false })

Expand Down
9 changes: 8 additions & 1 deletion packages/synapse-sdk/src/storage/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,14 @@ export class StorageContext {
options: StorageServiceOptions
): Promise<ProviderSelectionResult> {
const clientAddress = synapse.client.account.address
const requestedMetadata = combineMetadata(options.metadata, { withCDN: options.withCDN })
// CDN group is opt-in. Empty by default keeps the legacy one-rail-per-data-set behavior and
// exact-metadata reuse. When set (here or upstream in StorageManager), FWSS keys the shared
// bandwidth rail by keccak256(payer, group). When metadata was already combined upstream the
// withCDN key is present and combineMetadata leaves its value untouched, so this is idempotent.
const requestedMetadata = combineMetadata(options.metadata, {
withCDN: options.withCDN,
cdnGroup: options.cdnGroup,
})

// Handle explicit data set ID selection (highest priority)
if (options.dataSetId != null) {
Expand Down
40 changes: 38 additions & 2 deletions packages/synapse-sdk/src/storage/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export interface StorageManagerOptions {
warmStorageService: WarmStorageService
/** Whether to enable CDN services */
withCDN: boolean
/** CDN group id used as the `withCDN` metadata value (defaults to the payer address) */
cdnGroup?: string
/** Application identifier for namespace isolation */
source: string | null
}
Expand All @@ -148,6 +150,7 @@ export class StorageManager {
private readonly _synapse: Synapse
private readonly _warmStorageService: WarmStorageService
private readonly _withCDN: boolean
private readonly _cdnGroup?: string
private readonly _source: string | null
private _defaultContexts?: StorageContext[]

Expand All @@ -159,9 +162,29 @@ export class StorageManager {
this._synapse = options.synapse
this._warmStorageService = options.warmStorageService
this._withCDN = options.withCDN
this._cdnGroup = options.cdnGroup
this._source = options.source
}

/**
* Resolve the CDN group id used as the `withCDN` metadata value.
*
* The value FWSS keys the shared bandwidth rail by is `keccak256(payer, cdnGroup)`. Every data
* set sharing this value joins one bandwidth subscription, which is what lets a multi-copy CDN
* upload (copies > 1, on different providers) buy bandwidth once instead of once per copy.
*
* Opt-in: the group is empty by default, which keeps the legacy behavior of one bandwidth rail
* per data set (FWSS treats an empty group as "no subscription") and preserves exact-metadata
* data-set reuse. A caller shares CDN across copies by passing `cdnGroup` (here or on
* `Synapse.create`). The value must be stable across re-uploads, so the SDK reuses existing data
* sets on an EXACT `metadataMatches` rather than churning new ones. Resolving here (not per
* upload) keeps the primary and the secondary contexts created during retry/expansion on the
* same value.
*/
private _resolveCdnGroup(explicit?: string): string {
return explicit ?? this._cdnGroup ?? ''
}

/**
* The application source identifier used for dataset namespace isolation.
* Set via `Synapse.create({ source })`. Used by `combineMetadata` to tag
Expand Down Expand Up @@ -239,6 +262,7 @@ export class StorageManager {
explicitProviders,
signal: options?.signal,
withCDN: options?.withCDN,
cdnGroup: options?.cdnGroup,
metadata: options?.metadata,
pieceMetadata: options?.pieceMetadata,
callbacks: options?.callbacks,
Expand Down Expand Up @@ -414,6 +438,7 @@ export class StorageManager {
options?.contexts ??
(await this.createContexts({
withCDN: options?.withCDN,
cdnGroup: options?.cdnGroup,
copies: hasExplicitIds ? options?.copies : (options?.copies ?? DEFAULT_COPY_COUNT),
metadata: options?.metadata,
excludeProviderIds: options?.excludeProviderIds,
Expand All @@ -439,6 +464,7 @@ export class StorageManager {
explicitProviders: boolean
signal?: AbortSignal
withCDN?: boolean
cdnGroup?: string
metadata?: Record<string, string>
pieceMetadata?: Record<string, string>
callbacks?: Partial<CombinedCallbacks>
Expand Down Expand Up @@ -532,6 +558,7 @@ export class StorageManager {
try {
const [newContext] = await this.createContexts({
withCDN: options.withCDN,
cdnGroup: options.cdnGroup,
copies: 1,
metadata: options.metadata,
callbacks: options.callbacks,
Expand Down Expand Up @@ -880,7 +907,11 @@ export class StorageManager {
*/
async createContexts(options?: CreateContextsOptions): Promise<StorageContext[]> {
const withCDN = options?.withCDN ?? this._withCDN
const combinedMetadata = combineMetadata(options?.metadata, { withCDN, source: this._source })
// Resolve the CDN group here (not deeper) so every context in this group — the primary plus any
// secondaries created via _pullToSecondariesWithRetry, which re-enter createContexts — shares
// the same withCDN value and thus the same shared bandwidth rail in FWSS.
const cdnGroup = this._resolveCdnGroup(options?.cdnGroup)
const combinedMetadata = combineMetadata(options?.metadata, { withCDN, cdnGroup, source: this._source })
const canUseDefault = options == null || (options.providerIds == null && options.dataSetIds == null)
if (this._defaultContexts != null) {
const expectedSize = options?.copies ?? DEFAULT_COPY_COUNT
Expand Down Expand Up @@ -939,7 +970,12 @@ export class StorageManager {
async createContext(options?: StorageServiceOptions): Promise<StorageContext> {
// Determine the effective withCDN setting
const effectiveWithCDN = options?.withCDN ?? this._withCDN
const combinedMetadata = combineMetadata(options?.metadata, { withCDN: effectiveWithCDN, source: this._source })
const cdnGroup = this._resolveCdnGroup(options?.cdnGroup)
const combinedMetadata = combineMetadata(options?.metadata, {
withCDN: effectiveWithCDN,
cdnGroup,
source: this._source,
})

// Check if we can return the default context
// We can use the default if:
Expand Down
4 changes: 4 additions & 0 deletions packages/synapse-sdk/src/synapse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { WarmStorageService } from './warm-storage/index.ts'
*/
export class Synapse {
private readonly _withCDN: boolean
private readonly _cdnGroup?: string
private readonly _source: string | null
private readonly _payments: PaymentsService
private readonly _warmStorageService: WarmStorageService
Expand Down Expand Up @@ -86,6 +87,7 @@ export class Synapse {
return new Synapse({
client,
withCDN: options.withCDN,
cdnGroup: options.cdnGroup,
source: options.source,
sessionClient: options.sessionKey?.client,
})
Expand All @@ -96,6 +98,7 @@ export class Synapse {
this._sessionClient = options.sessionClient
this._chain = asChain(options.client.chain)
this._withCDN = options.withCDN ?? false
this._cdnGroup = options.cdnGroup
this._source = options.source ?? null
this._providers = new SPRegistryService({ client: options.client })
this._filbeamService = new FilBeamService(this._chain)
Expand All @@ -107,6 +110,7 @@ export class Synapse {
synapse: this,
warmStorageService: this._warmStorageService,
withCDN: this._withCDN,
cdnGroup: this._cdnGroup,
source: this._source,
})
}
Expand Down
14 changes: 12 additions & 2 deletions packages/synapse-sdk/src/test/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,29 @@ describe('Metadata Support', () => {
})

describe('withCDN option', () => {
it('should add withCDN key when withCDN is true', () => {
it('should add withCDN key with empty value when withCDN is true and no cdnGroup', () => {
const result = combineMetadata({}, { withCDN: true })
assert.deepEqual(result, { [METADATA_KEYS.WITH_CDN]: '' })
})

it('should add withCDN key with cdnGroup as value when provided', () => {
const result = combineMetadata({}, { withCDN: true, cdnGroup: '0xpayer' })
assert.deepEqual(result, { [METADATA_KEYS.WITH_CDN]: '0xpayer' })
})

it('should not add cdnGroup value when withCDN is false', () => {
const result = combineMetadata({}, { withCDN: false, cdnGroup: '0xpayer' })
assert.deepEqual(result, {})
})

it('should not add withCDN key when withCDN is false', () => {
const result = combineMetadata({}, { withCDN: false })
assert.deepEqual(result, {})
})

it('should not override existing withCDN key in metadata', () => {
const metadata = { [METADATA_KEYS.WITH_CDN]: '' }
const result = combineMetadata(metadata, { withCDN: true })
const result = combineMetadata(metadata, { withCDN: true, cdnGroup: '0xpayer' })
assert.deepEqual(result, { [METADATA_KEYS.WITH_CDN]: '' })
})
})
Expand Down
6 changes: 4 additions & 2 deletions packages/synapse-sdk/src/test/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,8 +615,10 @@ describe('StorageService', () => {
const serviceNoCDN = await StorageContext.create({ synapse, warmStorageService, withCDN: false })
assert.equal(serviceNoCDN.dataSetId, 1n, 'Should select non-CDN data set')

// Test with CDN = true
const serviceWithCDN = await StorageContext.create({ synapse, warmStorageService, withCDN: true })
// Test with CDN = true. The existing CDN data set has an empty withCDN value, so request an
// empty cdnGroup to match it (the default cdnGroup is the payer address, which would not
// match a legacy empty-value data set and would create a new one instead).
const serviceWithCDN = await StorageContext.create({ synapse, warmStorageService, withCDN: true, cdnGroup: '' })
assert.equal(serviceWithCDN.dataSetId, 2n, 'Should select CDN data set')
})

Expand Down
27 changes: 27 additions & 0 deletions packages/synapse-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ export interface SynapseOptions {
/** Whether to use CDN for retrievals (default: false) */
withCDN?: boolean

/**
* CDN group id used as the `withCDN` metadata value when CDN is enabled. FWSS keys the shared
* CDN bandwidth rail by `keccak256(payer, cdnGroup)`, so all data sets sharing this value join
* one bandwidth subscription instead of buying CDN per data set. Opt-in and empty by default,
* which keeps a dedicated bandwidth rail per data set and preserves exact-metadata reuse. Set the
* same value on every copy of a multi-copy upload so they share one rail.
*/
cdnGroup?: string

/**
* Application identifier for namespace isolation. When set to a non-empty string, datasets
* are tagged with this value and only datasets with a matching source are reused. Set to
Expand All @@ -154,6 +163,15 @@ export interface SynapseFromClientOptions {
/** Whether to use CDN for retrievals (default: false) */
withCDN?: boolean

/**
* CDN group id used as the `withCDN` metadata value when CDN is enabled. FWSS keys the shared
* CDN bandwidth rail by `keccak256(payer, cdnGroup)`, so all data sets sharing this value join
* one bandwidth subscription instead of buying CDN per data set. Opt-in and empty by default,
* which keeps a dedicated bandwidth rail per data set and preserves exact-metadata reuse. Set the
* same value on every copy of a multi-copy upload so they share one rail.
*/
cdnGroup?: string

/**
* Application identifier for namespace isolation. When set to a non-empty string, datasets
* are tagged with this value and only datasets with a matching source are reused. Set to
Expand Down Expand Up @@ -335,6 +353,15 @@ export interface BaseContextOptions {
/** Whether to enable CDN services */
withCDN?: boolean

/**
* CDN group id used as the `withCDN` metadata value when CDN is enabled. Every context (primary
* and secondaries) that shares this value resolves to the same shared CDN bandwidth rail in FWSS
* (keyed by `keccak256(payer, cdnGroup)`), so a multi-copy CDN upload buys bandwidth once instead
* of once per copy. Opt-in and empty by default, which keeps a dedicated bandwidth rail per data
* set and preserves exact-metadata reuse.
*/
cdnGroup?: string

/**
* Custom metadata for data sets (key-value pairs).
* Used to match existing data sets during provider selection.
Expand Down
7 changes: 6 additions & 1 deletion packages/synapse-sdk/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ export const METADATA_KEYS = {
* key does not strictly guarantee that CDN services will be provided, but the Warm Storage
* contract will attempt to enable payment for CDN services if this key is present.
*
* The value for this key is always an empty string.
* The value for this key is an optional CDN group id. FWSS keys the shared CDN bandwidth rail by
* `keccak256(payer, value)`, so every data set with the same value joins one bandwidth
* subscription instead of buying CDN once per data set (the case that matters for multi-copy
* uploads, where the copies must share a single bandwidth rail). The value is opt-in via the
* `cdnGroup` option and empty by default, which keeps today's behavior of a dedicated bandwidth
* rail per data set and preserves exact-metadata data-set reuse.
*
* Only valid for *data set* metadata.
*/
Expand Down
13 changes: 11 additions & 2 deletions packages/synapse-sdk/src/utils/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,29 @@ import { METADATA_KEYS } from './constants.ts'
* Each managed key is added only when its option is active AND the key is not already present
* in the metadata (explicit user metadata takes precedence).
*
* The `withCDN` key carries a CDN group id as its value. FWSS keys the shared CDN bandwidth rail
* by `keccak256(payer, value)`, so every data set that shares the same value joins one bandwidth
* subscription instead of buying CDN once per data set. Callers pass `cdnGroup` to set this value;
* when omitted, an empty string is used (today's behavior: one bandwidth rail per data set). The
* value must be identical across all copies of the same logical upload so they resolve to the same
* shared rail, and stable across re-uploads so exact-metadata reuse (`metadataMatches`) keeps
* finding the existing data sets instead of churning new ones.
*
* @param metadata - Base metadata object (can be empty)
* @param options - SDK-managed metadata options
* @param options.withCDN - Whether to include the CDN flag
* @param options.cdnGroup - CDN group id used as the `withCDN` value (empty string when omitted)
* @param options.source - Application identifier for namespace isolation (null or empty string to skip)
* @returns Combined metadata object
*/
export function combineMetadata(
metadata: Record<string, string> = {},
options?: { withCDN?: boolean; source?: string | null }
options?: { withCDN?: boolean; cdnGroup?: string; source?: string | null }
): Record<string, string> {
let result = metadata

if (options?.withCDN && !(METADATA_KEYS.WITH_CDN in result)) {
result = { ...result, [METADATA_KEYS.WITH_CDN]: '' }
result = { ...result, [METADATA_KEYS.WITH_CDN]: options.cdnGroup ?? '' }
}

if (options?.source && !(METADATA_KEYS.SOURCE in result)) {
Expand Down