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
6 changes: 6 additions & 0 deletions .changeset/two-meals-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@onflow/fcl-core": minor
"@onflow/fcl": minor
---

Decouple library from global `serviceRegistry` and `pluginRegistry`
174 changes: 174 additions & 0 deletions docs/architecture/api-design-discussion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# FCL API Design Discussion: Instance vs Context-Passing

**Date:** July 24, 2025
**Context:** Migration away from global state, evaluating instance-based vs context-passing patterns

## Background

FCL was migrated from global state to an instance-based approach:

```javascript
// Old global approach
mutate(transaction)

// New instance approach
const fcl = createFCL(config)
fcl.mutate(transaction)
```

The global functions still exist for backward compatibility, but now there are also context-bound functions in the instance.

## Question: Was this the right move vs tree-shakable context-passing?

## Option 1: Instance-Based Approach (Current)

```javascript
import { createFCL } from 'fcl-js'
const fcl = createFCL(config)
fcl.mutate(params)
fcl.query(script)
fcl.authenticate()
```

**Benefits:**
- ✅ Better developer experience (DX)
- ✅ Cleaner, more intuitive API
- ✅ Context binding prevents errors
- ✅ Better IDE autocomplete/TypeScript support
- ✅ Backward compatibility maintained
- ✅ Methods are discoverable on the instance

**Drawbacks:**
- ❌ Potentially larger bundles (imports entire instance)
- ❌ Less tree-shakable

## Option 2: Context-Passing Functions

```javascript
import { mutate, query, authenticate, createContext } from 'fcl-js'
const context = createContext(config)
mutate(context, params)
query(context, script)
authenticate(context)
```

**Benefits:**
- ✅ Better tree-shaking (only import what you use)
- ✅ Functional programming style
- ✅ Better bundle size optimization

**Drawbacks:**
- ❌ More verbose API
- ❌ Easy to forget context parameter
- ❌ Context gets passed everywhere
- ❌ Harder to discover available methods
- ❌ More imports needed

## Option 3: Overloaded Functions (Hybrid)

Support both patterns with function overloads:

```javascript
export function mutate(contextOrFirstArg, ...args) {
// Check if first arg is a context object
if (contextOrFirstArg && typeof contextOrFirstArg === 'object' && contextOrFirstArg._isContext) {
// New API: mutate(context, ...params)
return mutateFn(contextOrFirstArg, ...args)
} else {
// Legacy API: mutate(...params) - use global context
return mutateFn(getGlobalContext(), contextOrFirstArg, ...args)
}
}
```

**Usage Examples:**
```javascript
// Legacy (still works)
import { mutate } from 'fcl-js'
mutate(transaction, options)

// New context-passing (tree-shakable)
import { mutate, createContext } from 'fcl-js'
const context = createContext(config)
mutate(context, transaction, options)

// Instance-based (also still works)
const fcl = createFCL(config)
fcl.mutate(transaction, options)
```

**Benefits:**
- ✅ Zero breaking changes
- ✅ Tree-shakable for new users
- ✅ Clear migration path
- ✅ Single function handles both patterns
- ✅ Single API surface

## Option 4: Both Patterns Side-by-Side

```javascript
// Instance-based API
export function createFCL(config) {
const context = createContext(config)

return {
mutate: (...params) => mutate(context, ...params),
query: (...params) => query(context, ...params),
authenticate: (...params) => authenticate(context, ...params),
}
}

// Context-passing API (tree-shakable)
export { mutate, query, authenticate } from './core'
export { createContext } from './context'
```

## Analysis & Recommendation

### Tree-Shaking Considerations
- **Context-passing** is more tree-shakable
- **Instance-based** potentially bundles all methods
- For SDK-style libraries like FCL, users typically use multiple functions anyway

### Developer Experience (DX)
- **Instance-based** has significantly better DX
- **Context-passing** is more verbose and error-prone
- IDE support and discoverability favor instance approach

### Library Comparisons
- **React Query**: Uses hooks + context provider pattern
- **Zustand**: Primarily instance-based, supports context for SSR
- **Jotai**: Supports both global and explicit store patterns

### Final Assessment

**For FCL specifically:**

1. **Instance-based approach is better for DX** - the primary consideration for an SDK
2. **Context-passing has minimal tree-shaking benefit** since FCL users typically use multiple functions
3. **Overloaded functions provide best of both worlds** but add complexity
4. **Current instance approach strikes the right balance**

## Decision

**Stick with the instance-based approach** because:
- FCL is a cohesive SDK, not a utility library
- Developer experience is crucial for adoption
- Most users will use several functions together anyway
- Bundle size trade-off is acceptable for the DX benefits
- Maintains clean, intuitive API surface

The migration away from global state to instances was the right architectural choice.

## DX vs Tree-Shaking Trade-off Summary

| Aspect | Instance-Based | Context-Passing |
|--------|---------------|-----------------|
| **DX** | ✅ Excellent | ❌ Verbose |
| **Tree-shaking** | ❌ Limited | ✅ Excellent |
| **Discoverability** | ✅ Great | ❌ Poor |
| **Error-prone** | ✅ Low | ❌ High |
| **Bundle size** | ❌ Larger | ✅ Smaller |
| **API simplicity** | ✅ Clean | ❌ Complex |

**Conclusion:** For FCL, DX wins over tree-shaking optimization.
163 changes: 163 additions & 0 deletions docs/architecture/plugin-system-decoupling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Plugin System Decoupling - Implementation Summary

## Overview

The FCL plugin system has been successfully decoupled from global state while maintaining full backward compatibility. The system now supports both global (legacy) and context-aware (new) usage patterns.

## Changes Made

### 1. Core Plugin Functions Refactored

**File: `packages/fcl-core/src/current-user/exec-service/plugins.ts`**

- `ServiceRegistry` → `createServiceRegistry`: Now creates isolated service registries
- `PluginRegistry` → `createPluginRegistry`: Now creates isolated plugin registries
- Added `createRegistries()`: Factory function for context-aware registry pairs
- Maintained global registries for backward compatibility

### 2. Context Integration

**File: `packages/fcl-core/src/context/index.ts`**

- Added `serviceRegistry` and `pluginRegistry` to `FCLContext` interface
- Updated `createFCLContext()` to create context-specific registries
- Added optional `coreStrategies` parameter to configuration

**File: `packages/fcl-core/src/client.ts`**

- Added `coreStrategies` to `FlowClientCoreConfig` interface
- Exposed `serviceRegistry` and `pluginRegistry` on the client instance

### 3. Execution Service Updates

**File: `packages/fcl-core/src/current-user/exec-service/index.ts`**

- Added optional `serviceRegistry` parameter to `execService()` and `execStrategy()`
- Functions fall back to global registry when context registry not provided

### 4. Current User Integration

**File: `packages/fcl-core/src/current-user/index.ts`**

- Updated all `execService()` calls to pass context-aware `serviceRegistry`
- Added `serviceRegistry` parameter to current user context interfaces

## Usage Patterns

### 1. Global Registry (Backward Compatible)

```javascript
import { pluginRegistry } from '@onflow/fcl-core'

// This still works exactly as before
pluginRegistry.add({
name: "MyWalletPlugin",
f_type: "ServicePlugin",
type: "discovery-service",
services: [...],
serviceStrategy: { method: "CUSTOM/RPC", exec: customExecFunction }
})
```

### 2. Context-Aware Registries (New)

```javascript
import { createFlowClientCore } from '@onflow/fcl-core'

// Create client with custom core strategies
const fcl = createFlowClientCore({
accessNodeUrl: "https://rest-testnet.onflow.org",
platform: "web",
storage: myStorage,
computeLimit: 1000,
coreStrategies: {
"HTTP/POST": httpPostStrategy,
"IFRAME/RPC": iframeRpcStrategy,
"CUSTOM/RPC": myCustomStrategy
}
})

// Add plugins to this specific instance
fcl.pluginRegistry.add({
name: "InstanceSpecificPlugin",
f_type: "ServicePlugin",
type: "discovery-service",
services: [...],
serviceStrategy: { method: "INSTANCE/RPC", exec: instanceExecFunction }
})

// This plugin only affects this FCL instance, not others
```

### 3. Multiple Isolated Instances

```javascript
import { createFlowClientCore } from '@onflow/fcl-core'

// Testnet instance with its own plugins
const testnetFcl = createFlowClientCore({
accessNodeUrl: "https://rest-testnet.onflow.org",
platform: "web",
storage: testnetStorage,
computeLimit: 1000,
coreStrategies: testnetStrategies
})

// Mainnet instance with different plugins
const mainnetFcl = createFlowClientCore({
accessNodeUrl: "https://rest-mainnet.onflow.org",
platform: "web",
storage: mainnetStorage,
computeLimit: 1000,
coreStrategies: mainnetStrategies
})

// Add different plugins to each instance
testnetFcl.pluginRegistry.add(testnetSpecificPlugin)
mainnetFcl.pluginRegistry.add(mainnetSpecificPlugin)

// Each instance operates independently
```

### 4. Direct Registry Creation

```javascript
import { createRegistries } from '@onflow/fcl-core'

// Create registries directly for advanced use cases
const { serviceRegistry, pluginRegistry } = createRegistries({
coreStrategies: {
"HTTP/POST": myHttpStrategy,
"WEBSOCKET/RPC": myWebSocketStrategy
}
})

// Use the isolated registries
pluginRegistry.add(myPlugin)
const services = serviceRegistry.getServices()
```

## Benefits Achieved

1. **Zero Breaking Changes**: All existing code continues to work
2. **Instance Isolation**: Multiple FCL instances can have different plugin configurations
3. **Better Testing**: Each test can use isolated registries
4. **Reduced Global State**: Context-aware usage avoids global pollution
5. **Enhanced Flexibility**: Advanced users can create custom registry configurations

## Backward Compatibility

- Global `pluginRegistry` and `getServiceRegistry()` functions remain unchanged
- All existing plugin code will continue to work without modifications
- Legacy patterns are maintained while new patterns are available

## Migration Path

Developers can gradually migrate from global to context-aware patterns:

1. **Immediate**: Continue using global registries (no changes needed)
2. **Phase 1**: Start using `createFlowClientCore()` with instance-specific plugins
3. **Phase 2**: Gradually move plugin registrations to context-aware patterns
4. **Long-term**: Consider deprecating global registry usage in favor of context-aware patterns

This implementation provides the foundation for advanced FCL usage while maintaining the simplicity that makes FCL accessible to all developers.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/demo/src/components/flow-provider-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ export default function FlowProviderWrapper({
config={{
...flowConfig[flowNetwork],
appDetailTitle: "Demo App",
appDetailUrl: window.location.origin,
appDetailIcon: "https://avatars.githubusercontent.com/u/62387156?v=4",
appDetailUrl: "https://yourapp.com",
appDetailDescription: "Your app description",
computeLimit: 1000,
walletconnectProjectId: "9b70cfa398b2355a5eb9b1cf99f4a981",
}}
flowJson={flowJSON}
colorMode={darkMode ? "dark" : "light"}
Expand Down
3 changes: 3 additions & 0 deletions packages/demo/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ import react from "@vitejs/plugin-react"
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
allowedHosts: true,
},
})
3 changes: 3 additions & 0 deletions packages/fcl-core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export interface FlowClientCoreConfig {
transport?: SdkTransport
customResolver?: any
customDecoders?: any

// Core strategies for plugin system
coreStrategies?: any
}

export function createFlowClientCore(params: FlowClientCoreConfig) {
Expand Down
Loading