/** * The codec layer's contracts, written without naming any wire format: what a * codec is, the two things its state can mean to a walker, and what an * encode or a decode carries alongside the data. * * The distinction running through all of it is that a codec's type does not * say what its state is for. Every codec has the same members whatever domain * it works in, and the domains overlap, so which kind a codec is has to be * declared by the base class it extends rather than inferred from its * signature. Everything downstream -- what a registry will accept, how a * walker dispatches -- reads that declaration. */ import type { Constructor } from "@commonfabric/utils/types"; import type { FabricInstance, FabricValuePlus } from "@/interface.ts"; /** * Well-known symbol for binding the getter * `FabricClassWithNonterminalCodec[CODEC]`. */ export const CODEC: unique symbol = Symbol("data-model.codec"); /** * Well-known symbol for binding the static getter `[JSON_CODEC]` on a * `FabricPrimitive` class. * * No interface declares that obligation, and none can: {@link CodecRegistry} * reads a per-format codec through a symbol _variable_, so it must type a * class as `Partial>`, which no fixed-symbol interface * satisfies. The obligation is enforced when a registry is built instead -- * `registerClass()` throws for a class supplying no codec for the format in * play. {@link FabricClassWithNonterminalCodec} can exist only because * {@link CODEC} is statically known. * * A `FabricPrimitive`'s codec is bound per wire format, not once for all of * them, because these classes are where the formats disagree. `FabricBytes` * encodes to a base64url string, which is JSON's answer and nobody else's: a * format that carries bytes natively wants a different codec, or none, and * says so by looking up a different symbol. * * What differs can be the _kind_ of codec and not merely the state it * produces, which is why the binding is per format rather than a property of * the class. `FabricRegExp` expands into a record of strings under JSON, * which has no pattern type of its own to terminate into. * * That is what separates these from a `FabricInstance`'s codec, bound to the * generic `[CODEC]`: an instance's codec only expands an instance into other * `FabricValue`s and leaves every terminal decision to whatever walks the * result, so one binding serves every format. */ export const JSON_CODEC: unique symbol = Symbol("data-model.jsonCodec"); /** * Well-known symbol for binding the static getter `[REALM_CODEC]` on a * `FabricPrimitive` class. * * The realm-crossing counterpart to {@link JSON_CODEC}, and the reason that * one is bound per format rather than once. Structured cloning carries bytes * and `bigint` as themselves, so a class built on either has an answer here * that JSON cannot express: `FabricBytes` terminates into an `ArrayBuffer` * rather than base64url text, and both epoch types into a `bigint` rather * than a base64url encoding of one. * * A class binding a format-neutral `[CODEC]` needs nothing here. Every * `FabricInstance` is in that position, its codec expanding an instance into * other `FabricValue`s and leaving every terminal decision to whatever walks * the result. */ export const REALM_CODEC: unique symbol = Symbol("data-model.realmCodec"); /** * Interface for codecs (encoder-decoder objects). These are objects which can * extract "essential state" out of values (objects per se or otherwise) and * also take such "essential state" and produce values that are equivalent (in * a context-dependent sense) to the values that state was extracted from. * * `PlusType` is what the values this codec works on may hold beyond * `FabricValue`: the value side of every member is `FabricValuePlus`, * so a codec at `never` works over `FabricValue` exactly. `Encoded` is the * domain that essential state lives in. Every codec has the same shape * whatever those two are -- the same matching members, the same pair of * transformations -- and they are the only things that vary. {@link * NonterminalCodec} and {@link TerminalCodec} name the two ways the pair is * instantiated in practice, and are where the consequences are written down. * * The domain does not by itself say what the codec system should do with a * state, and it cannot: the domains overlap, in that an all-string record * satisfies `FabricValue` and JSON's value type alike. That is settled instead * by which base class a codec extends, which is where it stays: a registry * refuses a codec extending neither and otherwise stores it unaltered, and a * walker reads the class when it dispatches. * * `State` is the codec's own state type, a subtype of `Encoded`: what {@link * #encode} emits, what {@link #canDecode} narrows to, and the only thing {@link * #decode} is handed. One declaration serving all three members is what says * the three agree. A codec that works over the whole of `Encoded` leaves it at * the default, and so does whatever holds codecs that agree on nothing * narrower, as a registry does. */ export interface FabricCodec< PlusType, Encoded, State extends Encoded = Encoded, > { /** * The unique _direct_ class of instances, if any, that is associated with the * format this instance encodes. The codec system uses this to make a quick * determination about value compatibility before calling {@link #canEncode} * to confirm. */ get uniqueHandledClass(): Constructor | undefined; /** * The unique wire format tag that is associated with the format this instance * decodes from, or `undefined` for a codec with no single tag. When defined, * the codec system uses it to mark state produced by {@link #encode} and (by * default) routes state so marked back to this instance (or an equivalent) * for decoding; a codec with no tag is not registered for tag-based decode * dispatch. */ get recognizedTypeTag(): string | undefined; /** * Returns `true` if this handler can encode the state of the given value. * * May take `value` as a valid `FabricValuePlus`; see * `BaseCodecEngine.encode()` for the input contract that makes that safe to * assume. */ canEncode(value: FabricValuePlus): boolean; /** * Returns `true` if the given state is one this codec knows how to decode: * the decode side's counterpart to {@link #canEncode}, answering the same * kind of question about whether a value is in the domain this codec works * over. * * `state` is a member of `Encoded` already. Whether it is one at all is * settled by whatever produced it -- a wire format's parser, a realm * crossing, the codec engine's own walk -- and is not asked again here. What * is asked is narrower: whether that member has the shape of this codec's * `State`. So a part of the state which `State` allows to be any member of * `Encoded`, as an external reference such as a `FabricError`'s `cause` is, * leaves this method nothing to check. * * What belongs here is what is cheap to ask and not already asked by the * decoding: the state's type, the presence and types of the parts a decode * reads, membership in a fixed set of literals. What does not belong is a * check whose only implementation is the decode itself. Whether a string is * valid base64 is answered by decoding it, so asking here costs that work * twice; {@link #decode} keeps such a question and is where a state failing * it is refused. * * Stated as a type predicate over `State`, which is what makes the check * pay: the narrowing carries across to {@link #decode}, which then reads the * state's parts as the types this method established them to be. * * Called on every state before {@link #decode} sees it, so an implementation * of the latter may take the check as done. */ canDecode(state: Encoded): state is State; /** * Returns the wire type tag to use when encoding the given value. Only ever * called on a value for which {@link #canEncode} has returned `true`. Unlike * {@link #recognizedTypeTag} -- the codec's single recognized tag, if it has * one -- this is the concrete tag for a _specific_ value; a codec whose * instances each carry their own per-instance tag reads it from the value. */ tagForValue(value: FabricValuePlus): string; /** * Decodes a value from the given essential state, which is (alleged / * supposed) to be a value that was produced by an earlier call to {@link * #encode} on a compatible class to this one. The result is expected to be a * _shallow_ decoding; the codec system handles recursively converting `state` * contents as necessary. * * The given `typeTag` is what was associated with the given `state` and does * not necessarily correspond to {@link #recognizedTypeTag} (depending on how * an instance of this class got hooked up). * * Only ever called on a state for which {@link #canDecode} has returned * `true`, which is the decode side's counterpart to the way {@link * #canEncode} precedes {@link #encode}. That is what makes `state` a `State`, * whose parts an implementation reads as such. Where a codec is held at a * wider `State` than its own, as a registry holds every codec at `Encoded`, * the types do not carry that ordering, and it rests on the caller. * * A `state` need not come from {@link #encode}, so a nonterminal codec's * implementation which keeps a container from `state` as part of the value it * builds may do so only when that container is frozen and stays frozen in * that value. Anything else it keeps, it copies. A mutable value may keep * frozen state this way, so long as nothing that makes it mutable depends on * changing that state. A value from `state` which the codec's values hold as * an external reference is not a container of theirs, and is kept as it is. * What a terminal codec may keep from its state is its wire format's * business, as a transferred `ArrayBuffer` taken over whole shows. * * `mutable` decides the frozenness of the value built, and of nothing else: * when `false`, the default, the result is frozen, and when `true`, it is * left mutable. Either way a decode freezes only what it builds itself, * never a value it keeps from `state`; freezing what `state` holds belongs to * whatever built it. A codec whose values are immutable whatever their * construction, such as a `FabricPrimitive`'s, has nothing to decide and may * leave `mutable` undeclared. */ decode( typeTag: string, state: State, env: LiveEnvironment, mutable?: boolean, ): FabricValuePlus; /** * Encodes the given value, returning its essential state. This is only ever * called after {@link #canEncode} has confirmed that `value` is encodable by * this instance. The result is expected to be a _shallow_ encoding; the * codec system handles recursion as necessary. * * Two things an implementation may take as given: that `value` is a valid * `FabricValuePlus`, and that this instance's own {@link * #canEncode} has returned `true` for it. Re-checking either is work spent * on input that is correct by contract; see `BaseCodecEngine.encode()`. * * `env` is what a codec reaches the running system through, the same one * {@link #decode} is handed. * * For a nonterminal codec, the result is a snapshot of `value`: it * represents `value`'s internal state as frozen data, and its external * references as themselves, with their frozenness left as it is. This holds * whether or not `value` is itself frozen. A mutable value's internal state * is copied into the result, never frozen in place, and a result may be * cached so long as it is dropped when the value changes. A value whose * state effectively is an external reference may return that reference as * itself, frozen or not; so a caller must not freeze a result in place, or * otherwise change it. * * A terminal codec's result is in its wire format's own domain, and what it * may be is that format's business. It is best made as frozen as the format * allows: a record is frozen, though an `ArrayBuffer` in it cannot be. */ encode(value: FabricValuePlus, env: LiveEnvironment): State; } /** * A codec whose essential state is **nonterminal**: it is itself made of * `FabricValuePlus`s, which the walker goes on to expand in turn. * The sense is the one formal grammars give the word -- a state that arrives * here is not an answer but something that must be rewritten further before it * is one. * * Instantiating {@link FabricCodec} with its state domain equal to its value * domain is what says so: a state of that type is exactly what the walker * takes as input, so handing it one is handing it more work of the kind it * already does. A codec of this kind therefore settles nothing about how those * values are ultimately written down, and one instance serves every wire * format. * * `PlusType` is what the instances this codec exposes may hold, and its state * holds the same. At `never`, the default, both sides are `FabricValue`, which * is the one kind a wire format's registry takes. At any other `PlusType` the * codec exposes an instance's contents, that type included, to a walker that * admits it, and has no wire form. * * `State` is as {@link FabricCodec} describes it, bounded by that same domain. * It comes after `PlusType` so that it can default to the whole of the domain * it bounds. * * `FabricError` is the clearest case. Its state carries `cause` and every * `extraEntries()` pair, so it can hold arbitrary nested values -- including * other instances -- and only the walker can know what to do with them. */ export type NonterminalCodec< PlusType = never, State extends FabricValuePlus = FabricValuePlus, > = FabricCodec, State>; /** * A codec whose essential state is **terminal**: it is already in the domain of * one particular wire format, and the walker passes it through rather than * expanding it further. * * Instantiating {@link FabricCodec} at a format's own value type is what says * so. Such a codec is bound to that one format, and a class needing one * supplies a separate instance per format it participates in. * * `FabricBytes` is the clearest case: JSON's codec produces a base64url string, * where a format that carries bytes natively wants the bytes themselves, and no * one codec can answer both. * * The difference between the two kinds is not in the shape of a codec -- both * have the same members -- but in what its state means to the walker. A class * declares which it is by the base class it extends, and that declaration is * the only record of it. * * `Encoded` ranges over the wire formats' own value types. `FabricValue` is not * among them, and instantiating at it is unsound; see {@link * BaseTerminalCodec}. * * The value side is at `never`: a wire format carries `FabricValue`s, and a * value holding anything beyond those has no wire form to terminate into. * * `State` is as {@link FabricCodec} describes it: this codec's own states, * within the one format it serves. */ export type TerminalCodec = FabricCodec; /** * A codec usable for the wire format whose value type is `Encoded`: either * kind serves. A terminal codec serves that format alone, a nonterminal one * serves every format, and both are "for" this one. This is what a mixed * roster holds, what {@link CodecRegistry} stores, and what it hands back. * * Writing the union out is unavoidable. {@link FabricCodec} is covariant in * `Encoded`: its members are methods, whose parameters TypeScript compares * bivariantly, so the return position of `encode()` is what decides, and a * codec is assignable to an instantiation at a wider `Encoded` and to none at * a narrower one. `FabricValue` is a subtype of no format's value type, so a * `NonterminalCodec` is assignable to no format's instantiation, and the two * arms have to be named separately. * * The nonterminal arm is at `never`, the one `PlusType` with a wire form. That * is a claim about what a registry holds, not a check on what a class binds: * {@link CodecRegistry} reads a class's `[CODEC]` through a cast, so a codec at * another `PlusType` registers as this type, and what refuses its state is the * encode walk, which finds a value no codec claims. */ export type CodecForFormat = | NonterminalCodec | TerminalCodec; /** * A wire format, as {@link CodecRegistry} needs to know one: the type its * encoded states live in, and the symbol under which a class binds its codec * for this format. * * The two travel together because they name the same format, and a registry * given one of each separately could be given a mismatched pair. `Encoded` * appears in no member, so a descriptor is written with its type stated rather * than inferred from its contents. * * The symbol arrives as data rather than being known here, which is what keeps * this module from naming any particular format. */ export interface WireFormat { /** * Symbol under which a class binds the codec it supplies for this format. * Consulted only when the class binds no format-neutral `[CODEC]`, which * wins where a class has both. */ readonly codecSymbol: symbol; } /** * Interface for classes that provide a `NonterminalCodec` which is guaranteed * to operate on instances of the class. Binding here is the claim that one * codec serves every wire format. A class the formats want to treat * differently -- in the state produced, in the kind of codec, or both -- binds * one per format under that format's own symbol instead. * * `PlusType` is the class's own: a class whose instances are * `FabricInstancePlus` binds its codec at that same `PlusType`, so * that the state the codec exposes is typed for what the instances hold. * `codecOf()` types its result on that agreement and does not check it. */ export interface FabricClassWithNonterminalCodec { /** The codec instance to use for instances of this class. */ get [CODEC](): NonterminalCodec; } /** * The minimal interface that codec `encode()` and `decode()` implementations * may depend on: what a codec needs of the live system around it, and nothing * more. This package names what it requires rather than the classes that * happen to supply it, so a client brings its own. `BaseLiveEnvironment` is * the base to build one on, and `NullLiveEnvironment` covers a caller that * expects to need no cell. See Section 2.5 of the formal spec. */ export interface LiveEnvironment { /** * Resolves a cell reference, for a type that needs to intern or look up an * existing instance during decoding. */ getCell( ref: { id: string; path: string[]; space: string }, ): FabricInstance; }