Read-only Facets?

In large codebases I try to write my code defensively to prevent against (accidental) misuse that might invalidate assumptions I made when writing it. I find that I occasionally want the concept of a “read-only” facet-like thing.

Here’s a basic example:

const set = StateEffect.define<number | null>();

const MySpecialNumber = Facet.define<number | number>({
  combine: (f) => f[0] ?? 0,
});

function myExtensionFunction(myNumberClient: Client) {
    return [
      StateField.define<number>({
        create: () => myNumberClient.getInitialNumber(),
        update: (v, tr) => {
          for (const ef of tr.effects) {
            if (ef.is(set)) {
              v = ef.value;
            }
          }

          return v;
        },
        provide: (f) => MySpecialNumber.from(f),
      }),
      ViewPlugin.define((view) => {
        const dispose = myNumberClient.onNumberChanges(() => {
          view.dispatch({ effects: set.of(myNumberClient.number) });
        });

        return { destroy: () => dispose() };
      }),
    ];
 }

export { myExtensionFunction, /* Don't really want to export this --> */ MySpecialNumber };

Somewhere else in the codebase I want to read the value of MySpecialNumber. But I don’t want anywhere else in the codebase to register MySpecialNumber.of(...). Its value should be defined internally in this module only.

Until now I’ve just been exporting a getter wrapper, like:

function getMySpecialNumber(state) {
  return state.facet(MySpecialNumber)
}

But this kind of breaks down when I want to use MySpecialNumber in a dependency array for other computed things/ I don’t need to recompute every single time.

Wondering if there’s any other use cases for what I’m describing here. Thanks!

Nothing like that exists at the moment. What would it look like?

Perhaps like a wrapper class:

const ReadOnlyNumber = new ReadOnlyFacet(MySpecialNumber);
export { ReadOnlyNumber }

and that type can be used where facets are currently used:

state.facet(ReadOnlyFacet);
...
.compute([ReadOnlyFacet], (...) => ... )

but wouldn’t be able to register a value like facets can:

ReadOnlyFacet.of(...) // Does not exist

The same/ similar API could be used for state fields, for that matter.

There is also the use case of values which are just derived from something else. You can kind of do it like this:

const myComputedValue = Facet.define({})
const myFacet = Facet.define({
  enables: (f) => myComputedValue.compute([f], (state) => {
    return someComputedValue
  })
})

You may want to use the computed value here as a dependency somewhere, or maybe computing that value is quite expensive and so you only want it to be done once, so you create a facet for it. It would be nice if you could do something like:

const myFacet = Facet.define({})
const myComputedValue = computedFacet(myFacet, (state) => someComputedValue)

I’m not sure what the actual API here would look like (the one above isn’t great), but this would cover the read only case (as you can only read the computed value, not provide a value to it). You could do this with a state field, but then consumers may still try to mount that field themselves or try to change the value of the field (e.g. with init).

TLDR: basically saying that you could probably cover the read only case and this derived/computed value case at the same time.

Regarding computed values, given that defining an extra facet for them already allows you to do that, I don’t think that really warrants another library feature.

As for facet handles that don’t allow you to set the facet, this patch adds something like that purely on the type level. Does that look like it would work for you?

Nice! This looks like it should work. Thank you

Hey Marijn, I think this may have actually broken state.facet() typing. Now the type is reported as unknown:

https://replit.com/join/mjmgyeekko-bradyatreplit

image

import { python } from "@codemirror/lang-python";
import { EditorState, Facet } from "@codemirror/state";
import { EditorView, ViewPlugin } from "@codemirror/view";
import { basicSetup } from "codemirror";
import doc from './doc';

const AFacet = Facet.define<number>({});

let startState = EditorState.create({
  doc,
  extensions: [
    python(),
    basicSetup,
    AFacet.of(10),
    ViewPlugin.define((v) => {
      const value = v.state.facet(AFacet);

      sideEffectWithNumber(value);

      return {}
    })
  ],
})

new EditorView({
  state: startState,
  parent: document.body
})

function sideEffectWithNumber(num: number) {
  return num;
}


Good point. It seems the lack of the Output type in the FacetReader interface’s properties made TypeScript fail to preserve the type parameter when casting from Facet to FacetReader (and I didn’t notice because, in the CM codebase, the @internal properties of the interface are still present, avoiding the issue). This patch should improve that.