f8dde16a7e
Delegates the refuse-gate to ledgerDegraded(), lifts its console message into a pure testable fold, fixes stale doc line citations and an inaccurate outcome-enum comment, and splits the rename counter into collision-vs-sanitize.
216 lines
12 KiB
C++
216 lines
12 KiB
C++
#pragma once
|
|
// session — the ReaSamplerSession lifecycle owner of the persist seam. One
|
|
// class, three implementation TUs by responsibility:
|
|
// * session.cpp — poll() identity-transition detection (load / Save-As /
|
|
// forked sibling / recycled pointer) + the deferred undo/redo reload drain.
|
|
// * ext_state_io.cpp — save/load/writeAssignmentRequest: the ext-state <->
|
|
// JSON bridge, GUID minting, bank-folder relocation (see ext_state_io.h).
|
|
// * prune_fs.cpp — pruneDryRun/pruneOrphanSet/pruneReclaim: the prune
|
|
// scan and THE SINGLE FILE-DELETION AUTHORITY over user files in the bank
|
|
// folder, plus the one self-cleanup carve-out from it, both stated there.
|
|
//
|
|
// Save: BankModel JSON -> SetProjExtState under namespace "reasampler" (ext
|
|
// state lives inside the .rpp, so the index travels with the project for
|
|
// free). Load: GetProjExtState -> deserialize -> resolve each entry's bank
|
|
// file against the CURRENT project dir, so a project opened from a new
|
|
// location still finds its bank. Save-As: relocate the physical bank folder
|
|
// so the wavs end up under the new .rpp; the index's relative paths stay valid.
|
|
//
|
|
// REAPER-free header — all REAPER API calls live in the three TUs.
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/capture/tail_control.h"
|
|
#include "core/model/bank_book.h"
|
|
#include "core/model/bank_model.h"
|
|
#include "core/reclaim/prune_reconcile.h"
|
|
#include "core/tracking/origin_ledger.h"
|
|
#include "core/tracking/tracking_authority.h"
|
|
#include "core/version/app_version.h"
|
|
#include "core/view/view_mode_model.h"
|
|
|
|
namespace reasampler {
|
|
|
|
// Owns the session's BankBook (pool + named banks) and drives persistence
|
|
// against the active REAPER project. One instance lives for the extension's
|
|
// lifetime. Tracks the project identity last seen so the timer tick can
|
|
// detect a project load (a different project became active, so load the
|
|
// index from ext state) vs. a Save-As (same project, path changed, so
|
|
// relocate the bank folder under the new .rpp).
|
|
//
|
|
// Identity is layered GUID-primary: the minted GUID (content-based, immune to
|
|
// REAPER recycling a closed project's ReaProject* address) is checked first;
|
|
// the live pointer disambiguates only the same-GUID case — a forked sibling
|
|
// (same GUID, different object -> Load) vs. a genuine Save-As (same GUID,
|
|
// same object, new path -> relocate). Two prior designs each broke one
|
|
// direction: GUID-only misread a Save-As fork as the parent project;
|
|
// pointer-primary misread a recycled ReaProject* address as no-op. GUID-first
|
|
// catches recycling; the pointer then separates fork from Save-As.
|
|
//
|
|
// The book is exposed for the capture/action layer to mutate; persist only
|
|
// reads it on save and replaces it on load.
|
|
class ReaSamplerSession {
|
|
public:
|
|
ReaSamplerSession() = default;
|
|
|
|
// Pool + named banks + active-bank id; persist serializes under `banks`.
|
|
BankBook& book() { return book_; }
|
|
const BankBook& book() const { return book_; }
|
|
|
|
// The capture add-target: the active bank's BankModel (defaults to the pool).
|
|
model::BankModel& bank() { return book_.activeIndex(); }
|
|
const model::BankModel& bank() const { return book_.activeIndex(); }
|
|
|
|
// The project whose book/view/tail/ledger poll() last loaded — compare-only, never
|
|
// dereferenced; nullptr before the first poll. An action that can be fired against a
|
|
// project other than the one currently loaded here (the VST bake, which targets its own
|
|
// instance's tab) MUST check this before mutating: the book in memory belongs to one
|
|
// project, and landing another tab's request would write its capture into this bank.
|
|
const void* loadedProject() const { return lastProject_; }
|
|
|
|
// Design-View model; persists MODEL STATE only (visibility on open is the view shell's job).
|
|
ViewModeModel& view() { return view_; }
|
|
const ViewModeModel& view() const { return view_; }
|
|
|
|
// Docked panel's tail setting, authoritative here so it travels inside the .rpp.
|
|
capture::TailSetting& tail() { return tail_; }
|
|
const capture::TailSetting& tail() const { return tail_; }
|
|
|
|
// Record a system-created file at the moment it exists — the ONLY way a birth
|
|
// record is written. Lineage is read off the Sample's own provenance, the same
|
|
// act that stamped it, so the two cannot disagree; where they later diverge the
|
|
// ledger record is authoritative (core/tracking/CLAUDE.md).
|
|
//
|
|
// Call it at EVERY creation site regardless of the bank's AddResult: a
|
|
// hash-collapse still wrote a file the tool owns, and an unrecorded file is a
|
|
// permanently unreclaimable foreign file. A rejected record is reported to the
|
|
// console — a file with no record is the gap this track exists to eliminate.
|
|
//
|
|
// A consumer outside persist that needs the ledger must take it WITH its
|
|
// LedgerStatus (a tracking::TrackingState); no accessor exposes one without the
|
|
// other, because an absent record and an unreadable ledger demand opposite
|
|
// treatment.
|
|
void recordCreated(const model::Sample& sample, tracking::OriginKind kind);
|
|
|
|
// Whether a birth record can be written at all this session — the status WITHOUT
|
|
// the records. That is not a hole in the pairing rule above: the rule exists so an
|
|
// absent record is never read as a definite answer, and this exposes strictly less
|
|
// than the pair. The package import gates on it before it opens a file picker.
|
|
//
|
|
// A tradeoff, not the only route: `pruneDryRun()` already exposes the same degraded
|
|
// pair via `PruneReport::ledgerUnreadable`/`ledgerFutureVersion`, with no new
|
|
// accessor needed. Rejected because that route is genuinely worse for a gate: it
|
|
// drags a full bank-folder enumeration and every live instance's FX scan onto a
|
|
// check that only needs to know "can I write a record", and it shapes an import
|
|
// decision as an answer borrowed from prune's report rather than the session's own.
|
|
tracking::LedgerStatus ledgerStatus() const { return trackingStatus_; }
|
|
|
|
// The version that last wrote the active project: PreVersioning (no
|
|
// stamp), Unknown (malformed), or Stamped.
|
|
const version::WritingVersion& writingVersion() const { return writingVersion_; }
|
|
|
|
// Monotonic per project; recovered on load, written on every saveToActiveProject().
|
|
std::int64_t bankGeneration() const { return bankGeneration_; }
|
|
|
|
// Call at every bank-CONTENT mutation that changes what a live instance
|
|
// would play, NOT the organizational verbs (create/rename/reorder a
|
|
// bank). Rides the next persist. Over-bumping is safe; under-bumping
|
|
// misses a hands-free refresh, so call sites err toward bumping.
|
|
void bumpBankGeneration() { ++bankGeneration_; }
|
|
|
|
// Serializes book/view/tail to ext state, clears the retired legacy
|
|
// `bank_index` key. No-ops with no active/saved project. Returns true iff
|
|
// a persist happened, so a caller can skip an undo block when nothing was written.
|
|
//
|
|
// "Happened" is the writes being ISSUED, and a false means exactly two things —
|
|
// no active project, or an unsaved one — with NOTHING written under either. SIX
|
|
// callers turn a false straight into a discarded undo point, so this must never
|
|
// grow a third, OBSERVATIONAL failure mode: a false negative would silently
|
|
// remove the Ctrl-Z for a bank mutation that landed.
|
|
bool saveToActiveProject();
|
|
|
|
// Report-only prune dry-run: feeds the pure core with (present, referenced,
|
|
// owned) — `present` from the folder enumeration, the other two from the
|
|
// tracking authority. FAIL-SAFE: tracking state the authority cannot read
|
|
// sets blockedByTracking with an EMPTY orphan set. Read-only throughout.
|
|
reclaim::PruneReport pruneDryRun() const;
|
|
|
|
// The full (untruncated) orphan set, same compute as pruneDryRun. The
|
|
// prune action confirms this set before deleting it. Read-only.
|
|
std::vector<std::string> pruneOrphanSet() const;
|
|
|
|
// The resample's replace-vs-add input for one capture, gathered from the SAME live
|
|
// tracking state the prune scan reads, so the two consumers cannot disagree. Exposed
|
|
// as the answer rather than as the ledger, because an absent record and an unreadable
|
|
// ledger demand opposite treatment and only the pair says which. Read-only.
|
|
tracking::Answer tiedUsageFor(const std::string& capturePath,
|
|
const std::string& ownUsageKey) const;
|
|
|
|
// Delete the confirmed orphan set — the sole file-deletion path,
|
|
// callable only after an explicit user confirm. Re-enumerates and runs
|
|
// the pure core fresh, deleting exactly `confirmed ∩ freshOrphans` so a
|
|
// file that vanished or became referenced since confirm is skipped, and
|
|
// an orphan the user did not see is never swept. Trash-preferred
|
|
// (Windows Recycle Bin; unlink elsewhere). Does not modify the book or
|
|
// the ledger, writes no ext-state. No-ops when nothing to delete;
|
|
// does not prompt.
|
|
reclaim::PruneDeletionResult pruneReclaim(
|
|
const std::vector<std::string>& confirmed) const;
|
|
|
|
// Write the ingest assignment request (`assign_request` key): "the active
|
|
// sampler instance should now play THIS sample." `wire` is pre-encoded
|
|
// (assignment_request.h); a sibling one-shot write, not part of
|
|
// saveToActiveProject's blob. Same two-condition return as that call: false
|
|
// means no active/saved project and nothing was written, true means the write
|
|
// was ISSUED — never that the project took it.
|
|
bool writeAssignmentRequest(const std::string& wire);
|
|
|
|
// Detects a project load or Save-As and reacts. Driven by REAPER's
|
|
// "timer" register; idempotent per tick. Also drains a pending undo/redo
|
|
// reload (requestReload): the identity classifier alone would read an
|
|
// undo/redo as NoOp since identity is unchanged, so the projectconfig
|
|
// hook's reload flag is honored FIRST, before the identity check.
|
|
void poll();
|
|
|
|
// Request a reload of book_/view_/tail_ on the next poll() tick. Raised
|
|
// by the projectconfig hook only on an undo/redo state restore. Deferred
|
|
// because the hook fires BEFORE REAPER restores the <EXTSTATE> block —
|
|
// reading synchronously there would return the pre-undo value.
|
|
void requestReload();
|
|
|
|
// Load signal for the reapply-on-open glue: poll() raises this whenever
|
|
// it (re)loads the view model; consumeLoadSignal() returns true once and
|
|
// clears it. Signal-based since persist stays model-only (never calls
|
|
// the view shell); main.cpp owns the glue.
|
|
bool consumeLoadSignal();
|
|
|
|
private:
|
|
BankBook book_;
|
|
ViewModeModel view_; // reset to default on a project with no stored view_state
|
|
capture::TailSetting tail_; // reset to default (None / 2s) with no stored tail key
|
|
tracking::OriginLedger tracking_; // reset to empty/stored on EVERY load path, never inherited
|
|
// Written only by loadFromProject, so a degraded status is sticky until the
|
|
// project is reloaded (see this directory's CLAUDE.md).
|
|
tracking::LedgerStatus trackingStatus_ = tracking::LedgerStatus::Fresh;
|
|
version::WritingVersion writingVersion_; // recovered per load; PreVersioning default
|
|
std::int64_t bankGeneration_ = 0; // recovered per load (absent -> 0); monotonic
|
|
|
|
// Project identity last observed by poll(). GUID is primary; the pointer
|
|
// disambiguates the same-GUID case. Held as void* (compare-only, never
|
|
// dereferenced) so the header stays REAPER-free.
|
|
void* lastProject_ = nullptr;
|
|
std::string lastGuid_; // "" until the first saved project is seen
|
|
std::string lastRppPath_;
|
|
bool primed_ = false; // false until the first poll() observes state
|
|
bool loadPending_ = false; // raised by loadFromProject; drained by consumeLoadSignal
|
|
bool reloadRequested_ = false; // raised by requestReload; drained by poll
|
|
|
|
// Load the book from `proj`'s ext state (`banks`, else legacy
|
|
// `bank_index` migrated into the pool); also restores view_/tail_/tracking_.
|
|
void loadFromProject(void* proj, const std::string& projectDir);
|
|
};
|
|
|
|
} // namespace reasampler
|