| name | services-extension-consumption |
| description | Guidelines for consuming salesforcedx-vscode-services extension API. Use when working with extensions that have extensionDependency on salesforcedx-vscode-services, registering commands, using Workspace/Connection/Project/Settings/FS/Channel/Media services, quickpick/quickInput, implementing file/config watchers, editing extensionProvider.ts, buildAllServicesLayer, AllServicesLayer, setAllServicesLayer, prebuiltServicesDependencies, or Layer composition for VS Code extensions. |
Consuming salesforcedx-vscode-services
Extensions depending on salesforcedx-vscode-services. Examples: salesforcedx-vscode-metadata, salesforcedx-vscode-org-browser.
Getting the API
Use ExtensionProviderService from @salesforce/effect-ext-utils:
import { ExtensionProviderService, getServicesApi } from '@salesforce/effect-ext-utils';
const ExtensionProviderServiceLive = Layer.effect(
ExtensionProviderService,
Effect.sync(() => ({
getServicesApi
}))
);
const api = yield * (yield * ExtensionProviderService).getServicesApi;
Prebuilt vs Per-Extension Services
api.services.prebuiltServicesDependencies — pre-built Context.Context from services extension activation. Wrap with Layer.succeedContext(...).
Shares singleton instances (caches, watchers) across extensions; avoids re-building stateful services.
Per-extension layers (must build yourself):
| Layer | Why |
|---|
ChannelServiceLayer(displayName) | Own output channel |
ErrorHandlerService.Default | Depends on own ChannelService |
ExtensionContextServiceLayer(context) | Own ExtensionContext |
SdkLayerFor(context) | Own tracer (extension name/version in resource attributes) |
ExtensionProviderServiceLive | Local singleton |
ExtensionContext Setup
Preferred: import buildAllServicesLayer from @salesforce/effect-ext-utils. It reads displayName from package.json, falling back to the second arg. services/extensionProvider.ts only needs the mutable AllServicesLayer + setter:
import { buildAllServicesLayer } from '@salesforce/effect-ext-utils';
export let AllServicesLayer: ReturnType<typeof buildAllServicesLayer>;
export const setAllServicesLayer = (layer: ReturnType<typeof buildAllServicesLayer>) => {
AllServicesLayer = layer;
};
In activate — pass the context and a localized fallback channel name:
import { buildAllServicesLayer } from '@salesforce/effect-ext-utils';
import { nls } from './messages';
import { setAllServicesLayer } from './services/extensionProvider';
export const activate = async (context: vscode.ExtensionContext): Promise<void> => {
setAllServicesLayer(buildAllServicesLayer(context, nls.localize('channel_name')));
await getRuntime().runPromise(activateEffect(context));
};
Legacy inline pattern (still present in metadata, org, org-browser, lightning, visualforce, soql, apex-log, apex-testing): a local buildAllServicesLayer factory wraps Layer.unwrapEffect(...) in services/extensionProvider.ts. Migrate to the shared helper when touching these — drop the factory, import buildAllServicesLayer from @salesforce/effect-ext-utils, pass the fallback name at the call site.
Runtime vs provide
- Do: Build
ManagedRuntime.make(AllServicesLayer) and export getRuntime().
- Do: Use
getRuntime().runPromise(effect) / runFork(effect) for ad-hoc execution.
- Don't: Use
Effect.provide(AllServicesLayer) at call sites — use the runtime instead.
- Exception:
registerCommandWithLayer(AllServicesLayer) — keep passing the Layer; it internally uses provide.
Registering Commands
Use registerCommandWithLayer (for layers) or registerCommandWithRuntime (for runtimes):
import { myCommandEffect } from './commands/myCommand';
const api = yield * (yield * ExtensionProviderService).getServicesApi;
const registerCommand = api.services.registerCommandWithLayer(AllServicesLayer);
yield * registerCommand('sf.my.command', myCommandEffect);
const registerCommand = api.services.registerCommandWithRuntime(getRuntime());
yield * registerCommand('sf.my.command', myCommandEffect);
Commands auto:
- Register with ExtensionContext subscriptions
- Wrap with error handling
- Trace with observability spans
- Handle Cancellation
Success handling
Effect.fn accepts middleware args after the generator. Put success-side middleware before catchTag/catchAll — otherwise caught errors become successes.
export const deployActiveEditorCommand = Effect.fn('deploySourcePath.deployActiveEditor')(
function* () {
},
withConfigurableSuccessNotification(nls.localize('command_succeeded_text', label)),
Effect.catchTag('NoActiveEditorError', () =>
Effect.promise(() => vscode.window.showErrorMessage(nls.localize('deploy_select_file_or_directory'))).pipe(
Effect.as(undefined)
)
)
);
withConfigurableSuccessNotification wraps the effect with Effect.tap, so it only fires when the effect succeeds:
export const withConfigurableSuccessNotification =
(message: string) =>
<A, E, R>(effect: Effect.Effect<A, E, R>) =>
Effect.tap(effect, () =>
Effect.sync(() => {
const show = vscode.workspace.getConfiguration(SECTION).get<boolean>(KEY, false);
if (show) void vscode.window.showInformationMessage(message);
})
);
Invoking sf.org.login.web
Cross-extension / executeCommand: vscode.commands.executeCommand('sf.org.login.web', instanceUrl?, reauthAliasOrUsername?).
- No args: interactive flow (palette).
- With
instanceUrl: skips org-type quick pick.
- Second arg applies only when
instanceUrl was provided: trimmed non-empty string becomes the auth alias (access-token re-auth); else alias defaults to reauth-vscodeOrg.
Basic Services
Accessor pattern: call methods directly, don't assign to variable first.
Watchers
File Watching
FileWatcherService exposes a PubSub of all workspace file changes (**/*). Subscribe and filter:
import * as PubSub from 'effect/PubSub';
import * as Stream from 'effect/Stream';
const fileWatcher = yield * api.services.FileWatcherService;
yield* Stream.fromPubSub(fileWatcher.pubsub).pipe(
Stream.filter(event => ),
Stream.runForEach(event =>
Effect.sync(() => {
})
)
);
Config Watching
Watch VS Code config changes:
import * as PubSub from 'effect/PubSub';
import * as Stream from 'effect/Stream';
import * as Duration from 'effect/Duration';
const pubsub = yield * PubSub.sliding<vscode.ConfigurationChangeEvent>(100);
const disposable = vscode.workspace.onDidChangeConfiguration(event => {
Effect.runSync(PubSub.publish(pubsub, event));
});
yield *
Effect.addFinalizer(() =>
Effect.sync(() => {
disposable?.dispose();
})
);
yield *
Stream.fromPubSub(pubsub).pipe(
Stream.filter(event => event.affectsConfiguration('section.setting')),
Stream.debounce(Duration.millis(100)),
Stream.runForEach(() => {
})
);
Target Org Changes
Watch org changes via TargetOrgRef (SubscriptionRef):
const ref = yield * api.services.TargetOrgRef();
yield *
ref.changes.pipe(
Stream.map(org => org.orgId),
Stream.changes,
Stream.tap(orgId => {
}),
Stream.runForEach(() => {
})
);
ref.changes always emits the current value as element 0, then future changes. Never prepend an explicit get:
Stream.concat(Stream.fromEffect(SubscriptionRef.get(ref)), ref.changes)
Stream.concat(Stream.make(yield* SubscriptionRef.get(ref)), ref.changes)
ref.changes
To suppress the initial snapshot (e.g. avoid triggering a refresh before a tree provider is ready), use Stream.drop(1).
Ref behavior (concise):
- Default-org update: username from User SOQL when present; else AuthInfo login username on the connection.
TargetOrgRef snapshot without username: optional ConfigUtil.getUsername() (project default) before treating as no target org — see salesforcedx-vscode-org orgDisplay.
TargetOrgRef value is always an object (never undefined); only fields like orgId within it are optional.
Complete Example Pattern
import { buildAllServicesLayer } from '@salesforce/effect-ext-utils';
export let AllServicesLayer: ReturnType<typeof buildAllServicesLayer>;
export const setAllServicesLayer = (layer: ReturnType<typeof buildAllServicesLayer>) => {
AllServicesLayer = layer;
};
import * as ManagedRuntime from 'effect/ManagedRuntime';
import { AllServicesLayer } from './extensionProvider';
const createRuntime = () => ManagedRuntime.make(AllServicesLayer);
let _runtime: ReturnType<typeof createRuntime> | undefined;
export const getRuntime = () => (_runtime ??= createRuntime());
import { buildAllServicesLayer } from '@salesforce/effect-ext-utils';
import { nls } from './messages';
import { myCommandEffect } from './commands/myCommand';
import { AllServicesLayer, setAllServicesLayer } from './services/extensionProvider';
import { getRuntime } from './services/runtime';
export const activate = async (context: vscode.ExtensionContext) => {
setAllServicesLayer(buildAllServicesLayer(context, nls.localize('channel_name')));
await getRuntime().runPromise(activateEffect(context));
};
export const activateEffect = Effect.fn(`activation:${EXTENSION_NAME}`)(function* (_context: vscode.ExtensionContext) {
const api = yield* (yield* ExtensionProviderService).getServicesApi;
yield* api.services.ChannelService.appendToChannel('Extension activating');
const registerCommand = api.services.registerCommandWithLayer(AllServicesLayer);
yield* registerCommand('sf.my.command', myCommandEffect);
yield* api.services.ChannelService.appendToChannel('Extension activation complete.');
});
Common Patterns
- Start with
Layer.succeedContext(api.services.prebuiltServicesDependencies) — don't add individual *.Default for services already there
- Only add per-extension layers on top
import { ICONS } outside Effect; MediaService inside Effect
ChannelServiceLayer before ErrorHandlerService
- Pass
context to SdkLayerFor (extracts name/version from ExtensionContext)
Effect.forkIn(..., yield* getExtensionScope()) for watcher cleanup on deactivation
registerCommandWithLayer for all commands (tracing + error handling)
- Use
getRuntime().runPromise / runFork instead of Effect.provide(AllServicesLayer) for execution
Don't: rebuild services already in prebuiltServicesDependencies
return Layer.mergeAll(
ExtensionProviderServiceLive,
api.services.ExtensionContextServiceLayer(context),
api.services.FsService.Default,
api.services.AliasService.Default,
api.services.SdkLayerFor(context),
channelLayer,
errorHandlerWithChannel
);
return Layer.mergeAll(
Layer.succeedContext(api.services.prebuiltServicesDependencies),
ExtensionProviderServiceLive,
api.services.ExtensionContextServiceLayer(context),
api.services.SdkLayerFor(context),
channelLayer,
errorHandlerWithChannel
);
Review
Invoke the effect-advocate subagent on plans and diffs — its top-priority finding category is "you re-implemented something that already exists in salesforcedx-vscode-services."
prebuiltServicesDependencies contains ~27 services built once during services extension activation. Calling .Default on any of them creates a second instance with its own caches, watchers, and state — silently breaking cross-extension sharing.