Skip to content

refactor(services): move org-data VFS ownership from apex-testing to services - W-23596477 - #7912

Draft
peternhale wants to merge 3 commits into
developfrom
ph/W-23596477-org-data-vfs-services
Draft

refactor(services): move org-data VFS ownership from apex-testing to services - W-23596477#7912
peternhale wants to merge 3 commits into
developfrom
ph/W-23596477-org-data-vfs-services

Conversation

@peternhale

Copy link
Copy Markdown
Contributor

What does this PR do?

Consolidates all org-derived virtual-filesystem data under a single scheme (sf-org-data) owned by salesforcedx-vscode-services, and migrates apex-testing onto it as a pure consumer.

Services now owns:

  • orgVfs/ — the VFS provider, decoration provider, org-first URI layout (sf-org-data:/orgs/<orgKey>/<owner>/…), and a single org-change lifecycle reactor (close stale tabs, then purge the foreign org subtree).
  • The FsService org-data write API (writeOrgData / createOrgDataDir / deleteOrgData / clearOrgData) via a shared onOrgDataProvider helper. Writes route to the provider's *Internal methods; a non-sf-org-data/unregistered URI fails with a typed FsServiceError rather than a silent no-op.
  • An Effect-based FileSystemProviderRegistry (replacing the module-level fsProviderRef) and a shared closeMatchingTabs tab-reaper, both exposed on the services API surface.

apex-testing now:

  • Writes discovered classes through api.services.FsService.* — it holds no provider, registers no scheme, and no longer parses org-data paths.
  • Centralizes the class↔URI bijection in discoveryVfs/apexTestingClassUri.ts (encode + decode co-located).
  • Deletes its bespoke VFS provider, decoration provider, and provider-ref tag; owner attribution moves to services (decoration provider + sf:orgDataOwner context key + orgDataDocumentSelector).

The provider/editor surface stays read-only (isReadonly + FileSystemProvider mutators throw NoPermissions); owners populate the tree through the internal write API, which bypasses the read-only contract by design. This also fixes a latent dead-lens bug: a previewed non-Apex file could show a no-op "retrieve org-only test class" lens under the old per-scheme path parsing.

What issues does this PR fix or reference?

@W-23596477@

…services - W-23596477

Migrate the org-data virtual filesystem (scheme sf-org-data) so services owns the
provider, scheme registration, and org lifecycle, while apex-testing consumes it
through the FsService typed API as a side effect of test discovery.

Services now owns:
- orgVfs/ (provider, decoration provider, URI bijection, org lifecycle reconcile/reap)
- FsService org-data write API (writeOrgData/createOrgDataDir/deleteOrgData/clearOrgData)
  via a shared onOrgDataProvider helper
- fileSystemProviderRegistry + shared closeMatchingTabs tab-reaper (vscode/tabs.ts),
  exposed on the services API surface

apex-testing now:
- writes discovered classes through api.services.FsService.* (no provider of its own)
- centralizes the class<->URI bijection in discoveryVfs/apexTestingClassUri.ts
- deletes its bespoke VFS provider, decoration provider, and provider-ref tag

Provider/editor surface stays read-only (isReadonly + FileSystemProvider throws);
owners populate via the internal *Internal methods, bypassing the read-only contract.
@peternhale

Copy link
Copy Markdown
Contributor Author

Heads-up: the FsService org-data write API added here (writeOrgData/createOrgDataDir/deleteOrgData/clearOrgData) shipped without docs. Its fs-service reference section is documented in the stacked story-1 PR #7914 (along with the full sf-org-data VFS consumption surface), rather than split across the two PRs. No doc change needed here.

@@ -54,28 +53,27 @@ const toStat = (entry: Entry): vscode.FileStat => ({

const pathParts = (uri: URI): string[] => uri.path.split('/').filter(Boolean);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're working in vscode-uri, why not use its Utils for the "path math" stuff? (basename, pathParts, getParent, etc).

) {
const baseUri = resolveFindFilesBase(include);
const filePath = isString(include) ? include : include.pattern;
if (!baseUri) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the existing vscode.workspace.findFiles no-workspace behavior from an empty result to a rejected FsServiceError. VS Code documents that workspace.findFiles resolves with no results when no workspace folders are open (@types/vscode/index.d.ts:13302-13305), and the previous desktop implementation delegated directly to that API. Can this preserve [] for a string glob without a workspace rather than introduce a new error contract?


export class FsService extends Effect.Service<FsService>()('FsService', {
accessors: true,
dependencies: [],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FsService declares no dependency on FileSystemProviderRegistry, then obtains it with serviceOption, so FsService.Default can construct a partially functional service whose org-data methods fail only when called. servicesLayers.ts compensates with usage-site Layer.provide. Can the registry be a required declared service dependency (or the org-data operations move to an OrgDataService) so missing wiring fails during layer construction and FsService.Default has one complete meaning?

const sanitizeOrgKey = (orgKey: string): string => encodeURIComponent(orgKey.trim().toLowerCase());
const sanitizePathPart = (part: string): string => encodeURIComponent(part.trim());

export const orgRoot = (orgKey: string): URI =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdoc on these would be nice so consumers understand that orgKey is what...the orgId? [Or maybe better param naming if it's meant to be orgOd]

yield* fsService.clearOrgData({ orgKey, owner: OWNER });
const ownerRoot = api.services.orgDataOwnerRoot({ orgKey, owner: OWNER });
const classesRoot = api.services.orgDataUri({ orgKey, owner: OWNER, segments: [CLASSES_ROOT] });
yield* fsService.createOrgDataDir(ownerRoot);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these really necessary? Why not use the equivalent of safeWriteFile (from the fsService) which recursively writes the parent dirs (for example, if you had apexTestingClassDir construct the full URI?)

});
return fsService
.createOrgDataDir(Utils.dirname(classUri))
.pipe(Effect.andThen(fsService.writeOrgData(classUri, content)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, so there's separate write methods special for org data?

why not have fsService be smarter and say

  1. look at full URI
  2. figure out which fs that goes to based on scheme
  3. write there

so then consumers wanting to write "orgData" would just need to know the scheme and we could maybe provide just a helper for constructing URIs correctly ex: buildOrgUri(orgId, owner, subpath)

@@ -323,7 +322,7 @@
},
{
"command": "sf.apex.test.orgOnlyClass.retrieve",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aww, I didn't know this command had been created and that we'd put it in the palette.
If we didn't palette it, could we simplify this a lot?

ExtensionContextService,
ExtensionContextServiceLayer,
FileChangePubSub,
closeMatchingTabs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nobody is using this outside of services

* unavailable (older hosts) or nothing matches. Shared by any feature that reaps editor tabs backed
* by transient/virtual documents (e.g. the org-data VFS lifecycle and apex-testing retrieve flow).
*/
export const closeMatchingTabs = Effect.fn('closeMatchingTabs')(function* (predicate: (uri: URI) => boolean) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a scenario where you wouldn't want this to happen?

Like, I think exposing this as a "service" and making the consumers do it imperatively is maybe unnecessary, if instead services was like

  1. oh, I see you changed orgs
  2. lemme close all the org-fs-backed tabs for it
    etc

setExtensionContext(context);
const extensionScope = Effect.runSync(getExtensionScope());

const providerRegistry = makeFileSystemProviderRegistry();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to keep activate from growing a lot.

idea: have an effect to create your fsp, call it from the activate effect.
attach those to the extension scope with finalizers so vscode doesn't have to manage the context subscriptions (or if you really want to, that fsp setup can have a dep on the extensionContext that's already stashed by setExtensionContext)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I see, it has to done before fileSystemSetup. OK, the rest of the comment still stands.

const excludePattern = isString(exclude) ? exclude : exclude?.pattern;
const results: URI[] = [];
const visit = async (directory: URI, relativeDirectory: string): Promise<void> => {
if (token?.isCancellationRequested || results.length >= (maxResults ?? Number.POSITIVE_INFINITY)) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not put default on the optional param instead of doing this inline each time?


export type RegisteredFileSystemProvider = {
readonly provider: vscode.FileSystemProvider;
readonly findFiles?: (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be simpler to have fsService make memfs an exception, rather than making people pass these in? at least until we have another vfs use case that requires that level of abstraction?

see fsServiceL218 already uses the memfs schema as a conditional to use this bit at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants