import type { ModuleFilePath, PatchId, ValModules } from "@valbuild/core"; import type { Patch, ParentRef } from "@valbuild/shared/internal"; import { ValOps, type AuthorId, type BaseSha, type GenericErrorMessage, type MetadataOfType, type OpsMetadata, type OrderedPatches, type OrderedPatchesMetadata, type PatchGroupMembership, type PreparedCommit, type SaveSourceFilePatchResult, type SchemaSha, type SourcesSha, type ValOpsOptions, type WithGenericError } from "./ValOps.js"; import type { HistoryError } from "./history/HistoryError.js"; import type { AffectedFile, CommitPage, CommitPatch, HistoricalCommit, StoredModuleVersion } from "./history/types.js"; import { result } from "@valbuild/core/fp"; /** * One stored patch. Everything the ordered chain needs and nothing else. */ export type StoredPatch = { patchId: PatchId; path: ModuleFilePath; patch: Patch; authorId: AuthorId | null; createdAt: string; baseSha: BaseSha; }; /** * One pending binary file: an upload that has not been published yet. * * Keyed by the patch that carries it, exactly as `fs` mode keys the directory * it writes to. The patch's own `file` op holds a hash, not the bytes, so these * arrive on their own request and are joined up by `(patchId, filePath)`. */ export type StoredFile = { patchId: PatchId; /** * Where the file will live once published. * * For a LOCAL file that is `/public/val/photo.jpg`. For a REMOTE one it is * the path INSIDE the ref, not the ref itself -- `splitRemoteRef` has already * taken it apart by the time the bytes get here, and both readers are keyed * the same way. This is why neither takes `remote` into account: the caller * has resolved that before it asks. */ filePath: string; data: Buffer; metadata: MetadataOfType<"file" | "image"> | undefined; }; /** * Where pending patches live. * * The point of the interface: `ValOpsMemory` is not tied to memory. The default * implementation below is explicitly NOT durable -- it dies with the process, * or in a Worker with the isolate -- and a durable one (a Durable Object, whose * single-threaded execution is the lock Val's fs store builds out of a file) * is a swap rather than a rewrite. * * ORDER IS THE CONTRACT. `list()` returns patch ids in the order they were * written, and that order is the chain: entry i's parent is entry i-1. This is * the same decision `ValOpsFS` makes with `patches.log` -- the server decides * where a patch goes, and it goes last -- and it exists for the same reason: an * order held in the patches themselves lets a client working from a stale view * strand every patch behind a parent that never landed. */ export interface ValPatchStore { list(): Promise; get(patchId: PatchId): Promise; append(patch: StoredPatch): Promise; /** The patches AND every file they carry. */ delete(patchIds: PatchId[]): Promise; /** * Hold an uploaded file until its patch is published or dropped. * * Uploads arrive BEFORE the patch record does -- the record's `file` op * carries only a hash, so it would otherwise point at nothing. So a file for * a patch id that does not exist yet is normal and must be accepted. `fs` * mode stages these outside its store and moves them in when the record * lands, because a directory of files with no `patch.json` is indistinguishable * from a patch whose contents were lost, and its repair pass deletes those. * Nothing sweeps this store, so the two-step is not needed here -- at the cost * that bytes uploaded for a patch that is never recorded stay until the store * is dropped. */ putFile(file: StoredFile): Promise; getFile(patchId: PatchId, filePath: string): Promise; deleteFile(patchId: PatchId, filePath: string): Promise; /** Every file held for a patch, which is what the publish step uploads. */ filesOf(patchId: PatchId): Promise; } /** The default store. Not durable, deliberately and visibly. */ export declare class InMemoryPatchStore implements ValPatchStore { private readonly order; private readonly byId; list(): Promise; get(patchId: PatchId): Promise; append(patch: StoredPatch): Promise; delete(patchIds: PatchId[]): Promise; private static fileKey; private readonly files; putFile(file: StoredFile): Promise; getFile(patchId: PatchId, filePath: string): Promise; deleteFile(patchId: PatchId, filePath: string): Promise; filesOf(patchId: PatchId): Promise; } export type ValOpsMemoryOptions = ValOpsOptions & { /** * The project's source, by path, as the host already holds it. * * This is why the mode exists. `fs` mode reads `.val.ts` off a disk, and a * host that builds and publishes does not have one -- giving it a shimmed * filesystem to read through is what produced `Cannot access 'fs' before * initialization` and a `/stat` that long-polls watchers which cannot fire. * Here the host simply hands the source over. */ sourceFiles: Record; /** Where pending patches live. Defaults to memory; see ValPatchStore. */ patchStore?: ValPatchStore; /** * Serve without authenticating any request. Off by default. * * The name is the documentation. This mode runs deployed, so an * unauthenticated server is one where anyone who can reach the port can * create patches and drive a publish -- which is why the default is to * require a verified session like `http` mode does. * * A host sets this when it has its OWN boundary in front of Val and is * asserting that every request reaching here has already been authorised by * it. That is a real configuration, and it is not one to arrive at by * accident, so it is spelled out rather than inferred and it warns at * startup. */ unsafelyAllowUnauthenticated?: boolean; /** * Val's content host, for pushing remote files at publish. * * Only needed by {@link ValOpsMemory.uploadRemoteFiles}. A project with no * `s.image()` never reaches it. */ contentUrl?: string; }; /** * A `ValOps` for a host that is neither a developer's machine nor * content.val.build. * * EXPERIMENTAL -- see VAL_PROMPT.md. * * `fs` mode assumes a working tree it can watch and write; `http` mode assumes * the content API owns the patch chain and a commit means a git commit. A host * that builds and publishes its own output is neither: it holds the source * already, it has nowhere to watch, and its "commit" is a new build. * * What this deliberately does NOT do: * * - **No filesystem.** Source comes from `sourceFiles`, patches from a store. * - **No watching.** `getStat` still long-polls -- the hold is what paces the * client -- but it parks on a signal rather than racing a timer against an * mtime poll that can never observe anything here. Nothing can edit files * behind Val's back: source changes only when the host publishes, and that * replaces the process. * - **Pending binary files, but no PUBLISHED local ones.** An upload is held in * the patch store like any other pending change, so the Studio can preview it * before it is published. What this has no answer for is a file that is * already published and served from a `/public` directory: this configuration * uses Val's REMOTE files, where a published image lives on the content host * and the source carries a URL. `getBinaryFile` answers `null` for those -- * a miss, not a fault -- and `getBinaryFileMetadata` refuses by name. * - **No git history.** There is no repository here, so the history methods * answer `not-supported-in-fs-mode` -- the same closed error `ValOpsFS` * uses, so the History UI degrades the way it already knows how rather than * inventing a commit list. See the note above `listCommits`. */ export declare class ValOpsMemory extends ValOps { /** * The host's own store -- see {@link ValOps.patchesAreLocal}. `true` for the * same reason `fs` mode is: nothing is relayed to a content service, so * there is no session to verify against one and no group to separate authors * in. Where the two differ is not something a route asks about. */ readonly patchesAreLocal = true; /** * Required, unless the host explicitly takes the boundary itself. * * `patchesAreLocal` is true here and that is about publishing, not about who * may write -- see {@link ValOps.requiresAuth}. */ readonly requiresAuth: boolean; private readonly store; /** * The project's source, keyed WITHOUT a leading slash. * * Two spellings reach this. A host keys by project-relative path * (`src/routes/page.val.ts`) because that is what it built from; Val asks and * commits with a leading slash (`/src/routes/page.val.ts`). Normalised on the * way in so there is one entry per file — holding both spellings would let a * commit update one and leave the other as the stale answer. * * Not `readonly`: a save replaces the files it rewrote. See * {@link adoptPatchedSourceFiles}. */ private sourceFiles; private readonly contentUrl; constructor(valModules: ValModules, options: ValOpsMemoryOptions); onInit(): Promise; /** * Requests parked in {@link getStat}, waiting for something to happen. * * See there for why this exists. Resolved and emptied by * {@link announceChange}; never rejected, because a waiter that gives up does * so on its own timeout. */ private statWaiters; /** * Writes so far. Sampled before reading, compared after registering. * * The registration is not atomic with the read above it: a patch written * between `currentStat()` and `statWaiters.push` was announced to a list this * waiter was not yet on, so the request slept the full interval with a change * already sitting there. Comparing the count closes that window without a * lock. */ private changeCount; /** Wake every parked `getStat`. Called by this instance's own writes. */ private announceChange; private currentStat; getStat(params: { baseSha: BaseSha; schemaSha: SchemaSha; patches?: PatchId[]; } | null): Promise<{ type: "request-again" | "no-change" | "did-change"; baseSha: BaseSha; schemaSha: SchemaSha; sourcesSha: SourcesSha; patches: PatchId[]; }>; /** Resolves on the next write here, or when the poll interval runs out. */ private parkUntilChange; fetchPatches(filters: { patchIds?: PatchId[]; excludePatchOps: ExcludePatchOps; }): Promise; protected saveSourceFilePatch(path: ModuleFilePath, patch: Patch, patchId: PatchId, _parentRef: ParentRef | null, authorId: AuthorId | null, _sessionId: string | null, _patchGroup?: PatchGroupMembership): Promise; /** One spelling for a path, whichever the caller used. See sourceFiles. */ private static key; /** * A save has rewritten these files; they are the committed source now. * * Without this every save after the first re-reads the source as it was when * this object was built, applies only its own patches to that, and parks a * file that reverts everything saved before it -- with no error, because * applying the patch to the ORIGINAL text succeeds. The Studio auto-saves, so * that is not an edge case: it is most of a session's work. * * `fs` mode gets this for free -- `saveOrUploadFiles` writes the disk that * `getSourceFile` reads. There is no disk here, so it is written down. */ protected adoptPatchedSourceFiles(files: Record): void; protected getSourceFile(path: string): Promise>; deletePatches(patchIds: PatchId[]): Promise<{ deleted: PatchId[]; errors?: undefined; error?: undefined; }>; /** * Push this commit's pending binary files to Val's content host. * * The half of publishing that `commitPrepared` cannot do. Val's remote files * upload at PUBLISH, not when the image is added: until then the bytes are a * pending change like any other, held by {@link ValPatchStore}. So a publish * has to walk the descriptors and push each one before the source that * references it goes live -- otherwise the new build ships a URL that 404s. * * `ValOpsFS.saveOrUploadFiles` does the same loop, alongside two things this * has no use for: copying LOCAL binaries into a working tree, and writing the * source files (which is `commitPrepared` here). Kept separate rather than * shared, because the shapes only look alike. * * Errors are collected rather than thrown. One image that will not upload * should name itself and leave the rest of the publish decidable, rather than * failing a save that has already applied its patches. */ uploadRemoteFiles(preparedCommit: Pick, auth: { apiKey: string; } | { pat: string; }): Promise<{ uploaded: string[]; errors: Record; }>; private remoteOnly; saveBase64EncodedBinaryFileFromPatch(filePath: string, _parentRef: ParentRef, patchId: PatchId, data: string | null, _type: "file" | "image", metadata: MetadataOfType<"file" | "image"> | undefined): Promise>; getBase64EncodedBinaryFileFromPatch(filePath: string, patchId: PatchId, _remote?: boolean): Promise; protected getBase64EncodedBinaryFileMetadataFromPatch(filePath: string, type: T, patchId: PatchId, _remote?: boolean): Promise>; getBinaryFile(_filePathOrRef: string): Promise; protected getBinaryFileMetadata(_filePath: string, _type: T): Promise>; listCommits(): Promise>; getCommitPatches(): Promise>; getCommitModules(): Promise>; getCommitAffectedFiles(): Promise>; getFileAtCommit(): Promise>; gitPathOfModule(_moduleFilePath: ModuleFilePath): result.Result; } /** Kept exported so a host can name the error shape it may get back. */ export type ValOpsMemoryError = GenericErrorMessage;