Skip to content

Commit 3e6244a

Browse files
committed
Decouple FCL from global serviceRegistry and pluginRegistry
1 parent 0faefea commit 3e6244a

File tree

15 files changed

+510
-40
lines changed

15 files changed

+510
-40
lines changed

.changeset/two-meals-allow.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@onflow/fcl-core": minor
3+
"@onflow/fcl": minor
4+
---
5+
6+
Decouple library from global `serviceRegistry` and `pluginRegistry`
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# FCL API Design Discussion: Instance vs Context-Passing
2+
3+
**Date:** July 24, 2025
4+
**Context:** Migration away from global state, evaluating instance-based vs context-passing patterns
5+
6+
## Background
7+
8+
FCL was migrated from global state to an instance-based approach:
9+
10+
```javascript
11+
// Old global approach
12+
mutate(transaction)
13+
14+
// New instance approach
15+
const fcl = createFCL(config)
16+
fcl.mutate(transaction)
17+
```
18+
19+
The global functions still exist for backward compatibility, but now there are also context-bound functions in the instance.
20+
21+
## Question: Was this the right move vs tree-shakable context-passing?
22+
23+
## Option 1: Instance-Based Approach (Current)
24+
25+
```javascript
26+
import { createFCL } from 'fcl-js'
27+
const fcl = createFCL(config)
28+
fcl.mutate(params)
29+
fcl.query(script)
30+
fcl.authenticate()
31+
```
32+
33+
**Benefits:**
34+
- ✅ Better developer experience (DX)
35+
- ✅ Cleaner, more intuitive API
36+
- ✅ Context binding prevents errors
37+
- ✅ Better IDE autocomplete/TypeScript support
38+
- ✅ Backward compatibility maintained
39+
- ✅ Methods are discoverable on the instance
40+
41+
**Drawbacks:**
42+
- ❌ Potentially larger bundles (imports entire instance)
43+
- ❌ Less tree-shakable
44+
45+
## Option 2: Context-Passing Functions
46+
47+
```javascript
48+
import { mutate, query, authenticate, createContext } from 'fcl-js'
49+
const context = createContext(config)
50+
mutate(context, params)
51+
query(context, script)
52+
authenticate(context)
53+
```
54+
55+
**Benefits:**
56+
- ✅ Better tree-shaking (only import what you use)
57+
- ✅ Functional programming style
58+
- ✅ Better bundle size optimization
59+
60+
**Drawbacks:**
61+
- ❌ More verbose API
62+
- ❌ Easy to forget context parameter
63+
- ❌ Context gets passed everywhere
64+
- ❌ Harder to discover available methods
65+
- ❌ More imports needed
66+
67+
## Option 3: Overloaded Functions (Hybrid)
68+
69+
Support both patterns with function overloads:
70+
71+
```javascript
72+
export function mutate(contextOrFirstArg, ...args) {
73+
// Check if first arg is a context object
74+
if (contextOrFirstArg && typeof contextOrFirstArg === 'object' && contextOrFirstArg._isContext) {
75+
// New API: mutate(context, ...params)
76+
return mutateFn(contextOrFirstArg, ...args)
77+
} else {
78+
// Legacy API: mutate(...params) - use global context
79+
return mutateFn(getGlobalContext(), contextOrFirstArg, ...args)
80+
}
81+
}
82+
```
83+
84+
**Usage Examples:**
85+
```javascript
86+
// Legacy (still works)
87+
import { mutate } from 'fcl-js'
88+
mutate(transaction, options)
89+
90+
// New context-passing (tree-shakable)
91+
import { mutate, createContext } from 'fcl-js'
92+
const context = createContext(config)
93+
mutate(context, transaction, options)
94+
95+
// Instance-based (also still works)
96+
const fcl = createFCL(config)
97+
fcl.mutate(transaction, options)
98+
```
99+
100+
**Benefits:**
101+
- ✅ Zero breaking changes
102+
- ✅ Tree-shakable for new users
103+
- ✅ Clear migration path
104+
- ✅ Single function handles both patterns
105+
- ✅ Single API surface
106+
107+
## Option 4: Both Patterns Side-by-Side
108+
109+
```javascript
110+
// Instance-based API
111+
export function createFCL(config) {
112+
const context = createContext(config)
113+
114+
return {
115+
mutate: (...params) => mutate(context, ...params),
116+
query: (...params) => query(context, ...params),
117+
authenticate: (...params) => authenticate(context, ...params),
118+
}
119+
}
120+
121+
// Context-passing API (tree-shakable)
122+
export { mutate, query, authenticate } from './core'
123+
export { createContext } from './context'
124+
```
125+
126+
## Analysis & Recommendation
127+
128+
### Tree-Shaking Considerations
129+
- **Context-passing** is more tree-shakable
130+
- **Instance-based** potentially bundles all methods
131+
- For SDK-style libraries like FCL, users typically use multiple functions anyway
132+
133+
### Developer Experience (DX)
134+
- **Instance-based** has significantly better DX
135+
- **Context-passing** is more verbose and error-prone
136+
- IDE support and discoverability favor instance approach
137+
138+
### Library Comparisons
139+
- **React Query**: Uses hooks + context provider pattern
140+
- **Zustand**: Primarily instance-based, supports context for SSR
141+
- **Jotai**: Supports both global and explicit store patterns
142+
143+
### Final Assessment
144+
145+
**For FCL specifically:**
146+
147+
1. **Instance-based approach is better for DX** - the primary consideration for an SDK
148+
2. **Context-passing has minimal tree-shaking benefit** since FCL users typically use multiple functions
149+
3. **Overloaded functions provide best of both worlds** but add complexity
150+
4. **Current instance approach strikes the right balance**
151+
152+
## Decision
153+
154+
**Stick with the instance-based approach** because:
155+
- FCL is a cohesive SDK, not a utility library
156+
- Developer experience is crucial for adoption
157+
- Most users will use several functions together anyway
158+
- Bundle size trade-off is acceptable for the DX benefits
159+
- Maintains clean, intuitive API surface
160+
161+
The migration away from global state to instances was the right architectural choice.
162+
163+
## DX vs Tree-Shaking Trade-off Summary
164+
165+
| Aspect | Instance-Based | Context-Passing |
166+
|--------|---------------|-----------------|
167+
| **DX** | ✅ Excellent | ❌ Verbose |
168+
| **Tree-shaking** | ❌ Limited | ✅ Excellent |
169+
| **Discoverability** | ✅ Great | ❌ Poor |
170+
| **Error-prone** | ✅ Low | ❌ High |
171+
| **Bundle size** | ❌ Larger | ✅ Smaller |
172+
| **API simplicity** | ✅ Clean | ❌ Complex |
173+
174+
**Conclusion:** For FCL, DX wins over tree-shaking optimization.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Plugin System Decoupling - Implementation Summary
2+
3+
## Overview
4+
5+
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.
6+
7+
## Changes Made
8+
9+
### 1. Core Plugin Functions Refactored
10+
11+
**File: `packages/fcl-core/src/current-user/exec-service/plugins.ts`**
12+
13+
- `ServiceRegistry``createServiceRegistry`: Now creates isolated service registries
14+
- `PluginRegistry``createPluginRegistry`: Now creates isolated plugin registries
15+
- Added `createRegistries()`: Factory function for context-aware registry pairs
16+
- Maintained global registries for backward compatibility
17+
18+
### 2. Context Integration
19+
20+
**File: `packages/fcl-core/src/context/index.ts`**
21+
22+
- Added `serviceRegistry` and `pluginRegistry` to `FCLContext` interface
23+
- Updated `createFCLContext()` to create context-specific registries
24+
- Added optional `coreStrategies` parameter to configuration
25+
26+
**File: `packages/fcl-core/src/client.ts`**
27+
28+
- Added `coreStrategies` to `FlowClientCoreConfig` interface
29+
- Exposed `serviceRegistry` and `pluginRegistry` on the client instance
30+
31+
### 3. Execution Service Updates
32+
33+
**File: `packages/fcl-core/src/current-user/exec-service/index.ts`**
34+
35+
- Added optional `serviceRegistry` parameter to `execService()` and `execStrategy()`
36+
- Functions fall back to global registry when context registry not provided
37+
38+
### 4. Current User Integration
39+
40+
**File: `packages/fcl-core/src/current-user/index.ts`**
41+
42+
- Updated all `execService()` calls to pass context-aware `serviceRegistry`
43+
- Added `serviceRegistry` parameter to current user context interfaces
44+
45+
## Usage Patterns
46+
47+
### 1. Global Registry (Backward Compatible)
48+
49+
```javascript
50+
import { pluginRegistry } from '@onflow/fcl-core'
51+
52+
// This still works exactly as before
53+
pluginRegistry.add({
54+
name: "MyWalletPlugin",
55+
f_type: "ServicePlugin",
56+
type: "discovery-service",
57+
services: [...],
58+
serviceStrategy: { method: "CUSTOM/RPC", exec: customExecFunction }
59+
})
60+
```
61+
62+
### 2. Context-Aware Registries (New)
63+
64+
```javascript
65+
import { createFlowClientCore } from '@onflow/fcl-core'
66+
67+
// Create client with custom core strategies
68+
const fcl = createFlowClientCore({
69+
accessNodeUrl: "https://rest-testnet.onflow.org",
70+
platform: "web",
71+
storage: myStorage,
72+
computeLimit: 1000,
73+
coreStrategies: {
74+
"HTTP/POST": httpPostStrategy,
75+
"IFRAME/RPC": iframeRpcStrategy,
76+
"CUSTOM/RPC": myCustomStrategy
77+
}
78+
})
79+
80+
// Add plugins to this specific instance
81+
fcl.pluginRegistry.add({
82+
name: "InstanceSpecificPlugin",
83+
f_type: "ServicePlugin",
84+
type: "discovery-service",
85+
services: [...],
86+
serviceStrategy: { method: "INSTANCE/RPC", exec: instanceExecFunction }
87+
})
88+
89+
// This plugin only affects this FCL instance, not others
90+
```
91+
92+
### 3. Multiple Isolated Instances
93+
94+
```javascript
95+
import { createFlowClientCore } from '@onflow/fcl-core'
96+
97+
// Testnet instance with its own plugins
98+
const testnetFcl = createFlowClientCore({
99+
accessNodeUrl: "https://rest-testnet.onflow.org",
100+
platform: "web",
101+
storage: testnetStorage,
102+
computeLimit: 1000,
103+
coreStrategies: testnetStrategies
104+
})
105+
106+
// Mainnet instance with different plugins
107+
const mainnetFcl = createFlowClientCore({
108+
accessNodeUrl: "https://rest-mainnet.onflow.org",
109+
platform: "web",
110+
storage: mainnetStorage,
111+
computeLimit: 1000,
112+
coreStrategies: mainnetStrategies
113+
})
114+
115+
// Add different plugins to each instance
116+
testnetFcl.pluginRegistry.add(testnetSpecificPlugin)
117+
mainnetFcl.pluginRegistry.add(mainnetSpecificPlugin)
118+
119+
// Each instance operates independently
120+
```
121+
122+
### 4. Direct Registry Creation
123+
124+
```javascript
125+
import { createRegistries } from '@onflow/fcl-core'
126+
127+
// Create registries directly for advanced use cases
128+
const { serviceRegistry, pluginRegistry } = createRegistries({
129+
coreStrategies: {
130+
"HTTP/POST": myHttpStrategy,
131+
"WEBSOCKET/RPC": myWebSocketStrategy
132+
}
133+
})
134+
135+
// Use the isolated registries
136+
pluginRegistry.add(myPlugin)
137+
const services = serviceRegistry.getServices()
138+
```
139+
140+
## Benefits Achieved
141+
142+
1. **Zero Breaking Changes**: All existing code continues to work
143+
2. **Instance Isolation**: Multiple FCL instances can have different plugin configurations
144+
3. **Better Testing**: Each test can use isolated registries
145+
4. **Reduced Global State**: Context-aware usage avoids global pollution
146+
5. **Enhanced Flexibility**: Advanced users can create custom registry configurations
147+
148+
## Backward Compatibility
149+
150+
- Global `pluginRegistry` and `getServiceRegistry()` functions remain unchanged
151+
- All existing plugin code will continue to work without modifications
152+
- Legacy patterns are maintained while new patterns are available
153+
154+
## Migration Path
155+
156+
Developers can gradually migrate from global to context-aware patterns:
157+
158+
1. **Immediate**: Continue using global registries (no changes needed)
159+
2. **Phase 1**: Start using `createFlowClientCore()` with instance-specific plugins
160+
3. **Phase 2**: Gradually move plugin registrations to context-aware patterns
161+
4. **Long-term**: Consider deprecating global registry usage in favor of context-aware patterns
162+
163+
This implementation provides the foundation for advanced FCL usage while maintaining the simplicity that makes FCL accessible to all developers.

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/fcl-core/src/client.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export interface FlowClientCoreConfig {
5454
transport?: SdkTransport
5555
customResolver?: any
5656
customDecoders?: any
57+
58+
// Core strategies for plugin system
59+
coreStrategies?: any
5760
}
5861

5962
export function createFlowClientCore(params: FlowClientCoreConfig) {
@@ -85,6 +88,10 @@ export function createFlowClientCore(params: FlowClientCoreConfig) {
8588
// Utility methods
8689
serialize: createSerialize(context),
8790

91+
// Plugin system (context-aware)
92+
serviceRegistry: context.serviceRegistry,
93+
pluginRegistry: context.pluginRegistry,
94+
8895
// Re-export the SDK methods
8996
...context.sdk,
9097
}

packages/fcl-core/src/context/global.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
block,
1010
resolve,
1111
} from "@onflow/sdk"
12+
import {getServiceRegistry} from "../current-user/exec-service/plugins"
1213

1314
/**
1415
* Note to self:
@@ -24,7 +25,7 @@ import {
2425
*/
2526
export function createPartialGlobalFCLContext(): Pick<
2627
FCLContext,
27-
"config" | "sdk"
28+
"config" | "sdk" | "serviceRegistry"
2829
> {
2930
return {
3031
config: _config(),
@@ -37,5 +38,6 @@ export function createPartialGlobalFCLContext(): Pick<
3738
block,
3839
resolve,
3940
},
41+
serviceRegistry: getServiceRegistry(),
4042
}
4143
}

0 commit comments

Comments
 (0)