Services: delete dead RelationshipService; rename persistence to snippet-persistence

This commit is contained in:
2026-06-12 19:46:14 +03:00
parent 538882b342
commit 9a8b44380d
12 changed files with 86 additions and 209 deletions
+1 -1
View File
@@ -398,7 +398,7 @@ every keystroke. A startup subscriber observes the draft and debounces the expen
work:
```ts
// src/app/orchestration/persistence.ts
// src/app/orchestration/snippet-persistence.ts
import { useSnippetStore } from '../stores/SnippetStore';
import { saveSnippet } from '../infrastructure/snippet-store'; // IndexedDB adapter
+1 -1
View File
@@ -451,7 +451,7 @@ export async function saveSnippet(s: Snippet): Promise<void> {
fire-and-forget `void saveSnippet(n)` re-buries the very error the adapter took care to
throw. Persistence write-backs are wired as store subscribers, so the surfacing path is:
`orchestration/persistence.ts` (write-through `.catch`) / `orchestration/startup.ts` (load
`orchestration/snippet-persistence.ts` (write-through `.catch`) / `orchestration/startup.ts` (load
`.catch`, then run in memory) → `services/storage-errors.ts` (pure error→message mapper) →
`notify()` (`stores/NotificationStore`) → `Toaster`.
@@ -260,22 +260,14 @@ export function datasetUsageCounts(
): Map<string, number>;
```
```ts
// src/app/services/RelationshipService.ts — snapshot wrapper for non-reactive callers
import { snippetsReferencingDataset } from '../../core/relationships';
import { useSnippetStore } from '../stores/SnippetStore';
export function findSnippetsReferencingDataset(name: string): Snippet[] {
return snippetsReferencingDataset(useSnippetStore.getState().snippets, name);
}
```
The reactive UI does **not** go through the service: a component subscribed to
`snippets` calls the core helper directly, so the badge and Linked Snippets list
update the moment any snippet's draft changes its refs (auto-save) or it is
published — no duplicated matching logic, no `getState()` snapshot that would
miss updates.
There is no app-layer wrapper module around the scan. The reactive UI — a
component subscribed to `snippets` — calls the core helper directly, so the
badge and Linked Snippets list update the moment any snippet's draft changes
its refs (auto-save) or it is published. A non-reactive caller binds the same
helper to a snapshot inline at its call site
(`snippetsReferencingDataset(useSnippetStore.getState().snippets, name)`) —
one matching implementation, no `getState()` wrapper that would go stale in
reactive code.
**Do**
@@ -283,7 +275,7 @@ miss updates.
reactive and snapshot callers share it. It is O(snippets) but the collections
are small (library budget ~5 MB); clarity beats an index.
- Call the core helper directly from a store-subscribed component for reactivity;
use the service wrapper only from non-reactive (snapshot) code.
bind it to a `getState()` snapshot inline for non-reactive code.
**Don't**
@@ -334,9 +326,10 @@ export function dedupeIncomingDatasetNames(
```
> If imported snippets reference the renamed dataset, their `datasetRefs` and
> specs must be rewritten to the new name too — reuse the rename machinery in
> §6 over the imported snippet set, or run `renameDatasetEverywhere` per applied
> rename after the import is committed.
> specs must be rewritten to the new name too. The import flow does this purely,
> before anything is committed: `applyDatasetRenamesToSnippets`
> (`core/import-normalize`) rewrites the incoming snippet set per applied rename
> using the same `renameDatasetInSpec` machinery as §6.
### 5.1 Where the import/export flow lives, and its rules
@@ -427,43 +420,22 @@ export function renameDatasetInSpec(spec: Json, oldName: string, newName: string
}
```
```ts
// src/app/services/RelationshipService.ts — thin store coordinator
import { useDatasetStore } from '../stores/DatasetStore';
import { useSnippetStore } from '../stores/SnippetStore';
/**
* Renames a dataset and propagates the rename to every referencing snippet
* (spec, draftSpec, and datasetRefs). Returns the number of snippets changed.
* Caller is responsible for collision policy on `newName` (reject vs suffix).
*/
export function renameDatasetEverywhere(
oldName: string,
newName: string,
now?: Date,
): { updated: number } {
if (oldName === newName) return { updated: 0 };
// 1. Rename the dataset record itself.
const dataset = useDatasetStore.getState().datasets.find((d) => d.name === oldName);
if (dataset) useDatasetStore.getState().update(dataset.id, { name: newName }, now);
// 2 + 3. Delegate the spec/draftSpec/refs rewrite to the ONE rename impl — the
// store action that owns the snippet collection. It matches by spec+draft
// *content* (not `datasetRefs`, which mirrors only the draft) and recomputes
// `datasetRefs` from the rewritten draft. Keeping the loop in the store means
// the reactive editor buffer is refreshed in the same atomic update when the
// active snippet's draft is rewritten.
return { updated: useSnippetStore.getState().renameDatasetRefs(oldName, newName, now) };
}
```
The one rename implementation in the live app is the store action
**`SnippetStore.renameDatasetRefs(oldName, newName, now)`** — the action that
owns the snippet collection. It matches by spec+draft _content_ (not
`datasetRefs`, which mirrors only the draft), rewrites `spec`/`draftSpec` via
`renameDatasetInSpec`, recomputes `datasetRefs` from the rewritten draft, and
returns the number of snippets changed. Keeping the loop in the store means the
reactive editor buffer is refreshed in the same atomic update when the active
snippet's draft is rewritten. `DatasetStore.update` calls it whenever a save
changes a dataset's name, so a rename propagates everywhere as part of the one
user action — there is no separate coordinator module to call.
> **Collision on rename.** The UI rename form rejects a name already in use via
> `isNameTaken(newName, datasets, dataset.id)`. Programmatic renames (e.g. an
> import flow) instead resolve with `makeUniqueName` before calling
> `renameDatasetEverywhere`. The propagation function itself does not invent a
> name — it assumes `newName` is the agreed target.
> import flow) instead resolve with `makeUniqueName` first. The propagation
> action itself does not invent a name — it assumes `newName` is the agreed
> target.
**Do**
@@ -492,18 +464,17 @@ export function renameDatasetEverywhere(
## 7. Where things live
| Concern | Location | Pure? | Tested |
| ------------------------------------------------------------------------ | ----------------------------------------- | ------------------- | ----------- |
| `makeUniqueName`, `isNameTaken` | `src/core/naming.ts` | yes | unit |
| `extractDatasetRefs`, `recomputeDatasetRefs` | `src/core/spec-refs.ts` | yes | unit |
| `renameDatasetInSpec` | `src/core/spec-refs.ts` | yes | unit |
| `snippetsReferencingDataset`, `datasetUsageCounts` (reverse-lookup scan) | `src/core/relationships.ts` | yes | unit |
| `findSnippetsReferencingDataset`, usage count (snapshot wrappers) | `src/app/services/RelationshipService.ts` | no (reads store) | integration |
| `renameDatasetEverywhere``{ updated }` | `src/app/services/RelationshipService.ts` | no (mutates stores) | integration |
| `dedupeIncomingDatasetNames` | `src/core/import-normalize.ts` | yes | unit |
| Concern | Location | Pure? | Tested |
| ------------------------------------------------------------------------ | -------------------------------- | ------------------- | ----------- |
| `makeUniqueName`, `isNameTaken` | `src/core/naming.ts` | yes | unit |
| `extractDatasetRefs`, `recomputeDatasetRefs` | `src/core/spec-refs.ts` | yes | unit |
| `renameDatasetInSpec` | `src/core/spec-refs.ts` | yes | unit |
| `snippetsReferencingDataset`, `datasetUsageCounts` (reverse-lookup scan) | `src/core/relationships.ts` | yes | unit |
| `renameDatasetRefs` → updated count (rename propagation) | `src/app/stores/SnippetStore.ts` | no (mutates stores) | integration |
| `dedupeIncomingDatasetNames` | `src/core/import-normalize.ts` | yes | unit |
The dividing line: anything that takes plain data and returns plain data is
**core** and unit-tested in isolation; anything that reaches into a Zustand store
is an **app service**. The rule of thumb — _the **draft** spec is the source of
is a **store action or app service**. The rule of thumb — _the **draft** spec is the source of
truth, `datasetRefs` mirrors it, the reverse lookup is derived_ — is what keeps
the bidirectional link from ever needing manual repair.
@@ -138,7 +138,7 @@ control and freedom," #5 "error prevention"; spec §10 Reliability).
- **No silent data loss.** Edits auto-save as a **draft**; a known-good **published**
version is always preserved separately (§03D). A failed persist **surfaces as a toast**,
never a swallowed promise (`orchestration/persistence.ts`).
never a swallowed promise (`orchestration/snippet-persistence.ts`).
- **A marked exit from every committed change.** Revert restores the published spec;
Escape closes modals; delete/revert/reset require confirmation first.
- **Resilient rendering recovers on its own.** An invalid spec shows a readable error and