@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
138 lines • 6.23 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { CAS_MAX_RETRIES, generationOf } from "../record/types.js";
/**
* Provider for a workspace's whole control-plane state.
*
* One store, five planes, each scoped by workspace id: namespace nodes
* (symlinks, attribute overlays), observer events (command history),
* the session table (mount grants, cwd, env), the workspace metadata
* record (discovery: which sessions exist, which is the default), and
* the jobs (each session's job list and each job's console). Handing two
* processes the same store config plus a workspace id gives them the same
* workspace state, which is the seam that lets a kernel tier bind a
* session-bound mountpoint created by another daemon, and lets any of
* them list, wait on and kill a session's jobs.
*
* The node table and the session table are two NamespaceStores, the
* observer keeps its ObserverStore and the jobs their JobStore; this
* provider only unifies their construction, connection, and key scoping. Per-group overrides
* redirect a plane group to a different provider (e.g. large observer
* logs elsewhere while everything else stays on Redis). Sessions and
* metadata form one inseparable group: the default-session pointer in
* the metadata must never live on a different server than the session
* table it points into.
*/
export class WorkspaceStateStore {
namespaceOverride;
observerOverride;
workspaceOverride;
jobsOverride;
constructor(overrides = {}) {
this.namespaceOverride = overrides.namespace ?? null;
this.observerOverride = overrides.observer ?? null;
this.workspaceOverride = overrides.workspace ?? null;
this.jobsOverride = overrides.jobs ?? null;
}
/** The namespace plane (nodes) for one workspace. */
namespace(workspaceId) {
return (this.namespaceOverride ?? this).makeNamespace(workspaceId);
}
/** The observer plane (history events) for one workspace. */
observer(workspaceId) {
return (this.observerOverride ?? this).makeObserver(workspaceId);
}
/** The session table for one workspace. */
sessions(workspaceId) {
return (this.workspaceOverride ?? this).makeSessions(workspaceId);
}
/** The job plane (job lists, consoles, leases) for one workspace. */
jobs(workspaceId) {
return (this.jobsOverride ?? this).makeJobs(workspaceId);
}
/** Read one workspace's metadata record; null when never written. */
loadMeta(workspaceId) {
return (this.workspaceOverride ?? this).readMeta(workspaceId);
}
/**
* Write the metadata record iff its stored generation matches.
*
* Same optimistic-concurrency contract as NamespaceStore.casSet: a
* missing record (and a legacy record without the field) counts as
* generation 0, so create-if-absent is `expectedGeneration = 0`.
* Resolves true when the write landed, false on conflict.
*/
casSetMeta(workspaceId, fields, expectedGeneration) {
return (this.workspaceOverride ?? this).casWriteMeta(workspaceId, fields, expectedGeneration);
}
/**
* CAS-write `fields` over the stored record, retrying on conflict.
*
* Ours-wins content (snapshot restore semantics): each attempt
* merges `fields` over the stored record, preserves the stored
* `created_at`, bumps the generation, and retries when another
* writer got there first. Resolves with the record as written.
*/
async replaceMeta(workspaceId, fields) {
for (let attempt = 0; attempt < CAS_MAX_RETRIES; attempt++) {
const existing = await this.loadMeta(workspaceId);
const stored = existing ?? {};
const expected = generationOf(existing);
const merged = {
...stored,
...fields,
created_at: stored.created_at ?? Date.now() / 1000,
generation: expected + 1,
};
if (await this.casSetMeta(workspaceId, merged, expected))
return merged;
}
throw new Error(`workspace ${workspaceId} meta kept conflicting with another writer`);
}
/**
* Delete everything this store holds for one workspace.
*
* Deleting a workspace calls this and closing one does not, which is
* what lets a daemon's workspaces survive a restart. Each plane is
* cleared through its own store, then every provider involved forgets
* the workspace (its metadata record and the handles it keeps for the
* id), so a workspace created later under the same id starts empty.
*/
async drop(workspaceId) {
await this.namespace(workspaceId).clear();
await this.observer(workspaceId).clear();
await this.sessions(workspaceId).clear();
await this.jobs(workspaceId).clear();
const providers = new Set([this]);
for (const override of this.overrides())
providers.add(override);
for (const provider of providers)
await provider.forgetSelf(workspaceId);
}
/** Release connections held by this provider and its overrides. */
async close() {
for (const override of this.overrides())
await override.close();
await this.closeSelf();
}
overrides() {
return [
this.namespaceOverride,
this.observerOverride,
this.workspaceOverride,
this.jobsOverride,
].filter((override) => override !== null);
}
}
//# sourceMappingURL=base.js.map