Skip to content

Latest commit

 

History

History
118 lines (92 loc) · 6.96 KB

File metadata and controls

118 lines (92 loc) · 6.96 KB

Data layer (backend taxonomy)

The Data Layer

Persistence in src/app/repo/ is not one generic base class — it's a small taxonomy of backends, each suited to a different shape of data. Pick by matching an existing entity with the same size/query/lifecycle needs (see Adding an entity below), not by defaulting to Repo<T> for everything:

Backend Backs Use for Base file
Repo<T> chrome.storage.local Small config/metadata objects, optionally cached in memory repo.ts
DAO<T> Dexie (IndexedDB) Data needing indexed queries (where/pagination) dao.ts
OPFSRepo Origin Private File System Larger JSON blobs / files that don't fit a key-value store (Agent chat history, skills) opfs_repo.ts
Custom repository Purpose-built Only when none of the above fit (e.g. TrashScriptDAO) e.g. trash_script.ts

Repo<T>

export abstract class Repo<T> {
  useCache = false;
  constructor(protected prefix: string) {
    if (!prefix.endsWith(":")) this.prefix += ":";   // every key is "<prefix>:<key>"
  }
  enableCache() { this.useCache = true; }            // load-once, serve from memory
  protected joinKey(key: string) { return this.prefix + key; }
  protected _save(key, val): Promise<T> { /* cache or storage */ }
  get(key): Promise<T | undefined>;
  gets(keys): Promise<(T | undefined)[]>;
  getRecord(keys): Promise<Partial<Record<string, T>>>;
  find(filter?): Promise<T[]>;  findOne(filter?): Promise<T | undefined>;  all(): Promise<T[]>;
  update(key, val): Promise<T | false>;  updates(keys, val);
  delete(key): Promise<void>;  deletes(keys): Promise<void>;
}

Design notes:

  • Key scheme: entities are stored under "<prefix>:<key>" so a single chrome.storage.local namespace holds every entity type without collisions. find/all scan the prefix.
  • Cache: enableCache() switches reads/writes to a process-local cache that mirrors storage — used for hot collections (scripts) to avoid repeated async reads. A subclass that overrides joinKey can hash keys (e.g. resources keyed by URL via a UUID-v5 namespace).
  • Storage errors are logged, not thrownchrome.runtime.lastError is checked and reads continue, since a transient storage hiccup should not crash the worker.

Repository inventory

Names ending in DAO don't all share one base class — check which backend before copying a pattern.

Repo<T> (chrome.storage.local)

Repo File Entity Notes
ScriptDAO scripts.ts Script (metadata) Cached; companion ScriptCodeDAO stores source separately to keep metadata reads small; dedup via searchExistingScript
ValueDAO value.ts Value (GM storage) Keyed by storage name (per-script or shared @storageName)
ResourceDAO resource.ts Resource (@require/@resource) Overrides joinKey to hash URLs; CompiledResourceDAO caches compiled deps with a version namespace
PermissionDAO permission.ts Permission Composite key <uuid>:<permission>:<value>
SubscribeDAO subscribe.ts Subscribe Keyed by feed URL
FaviconDAO, LocalStorageDAO, ExportDAO, TempStorageDAO src/app/repo/*.ts misc Same Repo<T> pattern
AgentModelRepo agent_model.ts AgentModelConfig Agent model configs — small, no indexed query need
AgentTaskRepo agent_task.ts AgentTask Scheduled agent task definitions
MCPServerRepo mcp_server_repo.ts MCPServerConfig MCP server configs

DAO<T> (Dexie/IndexedDB)

DAO File Entity Notes
LoggerDAO logger.ts Logger Logs need indexed queries (where/pagination) that Repo<T>'s prefix scan doesn't give you

OPFSRepo (Origin Private File System)

Repo File Entity Notes
AgentChatRepo agent_chat.ts conversation metadata + messages Can grow large and holds attachments; stored under agents/conversations/
AgentTaskRunRepo agent_task.ts task run history Companion to AgentTaskRepo for run records
SkillRepo skill_repo.ts skill .md/script bundles Stored under agents/skills/

Custom repository (neither of the above fits)

Repo File Notes
TrashScriptDAO trash_script.ts Purpose-built for the trash/restore flow; doesn't extend Repo<T> or DAO<T>

Current exact set: git grep -nE 'class [A-Za-z0-9_]*(DAO|Repo)' -- src/app/repo.

Adding an entity

Don't default to Repo<T>. Decide by the same questions the existing choices above answer:

  • Data size and shape — small config object → Repo<T>; larger blob/file-like data → OPFSRepo.
  • Query pattern — need indexed where/pagination queries → DAO<T> (Dexie); simple get/set/prefix-scan → Repo<T>.
  • Lifecycle — matches an existing entity's access pattern closely → copy that entity's backend, not a generic template.

Only once you've picked Repo<T> (the common case for small config-shaped entities) does this shape apply:

export interface MyEntity { id: string; data: Record<string, unknown>; createtime: number; }

export class MyEntityDAO extends Repo<MyEntity> {
  constructor() { super("myentity"); }            // → keys "myentity:<id>"
  save(e: MyEntity) { return this._save(e.id, e); }
  findById(id: string) { return this.get(id); }
}

For DAO<T> or OPFSRepo, follow the nearest existing entity of that backend instead (e.g. LoggerDAO for Dexie, AgentChatRepo/SkillRepo for OPFS) — their construction and access patterns differ from Repo<T>.

Then pick an owner — "construct in the manager, expose via group.on" is one option, not the rule. Route through a context composition root only when the entity is genuinely owned by it and has to be reachable over RPC — that's why scriptDAO is built in ServiceWorkerManager (index.ts). Agent entities mostly aren't: AgentModelService falls back to new AgentModelRepo() internally when no repo is injected (model_service.ts), and AgentChatRepo is a module-level singleton (export const agentChatRepo in agent_chat.ts) imported directly. Copy the ownership style of the nearest entity on the same backend in the same subsystem.