mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 10:12:34 +00:00
Persistence: share entity write-through helper; normalize quota at db.put
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { StorageQuotaError } from '../infrastructure/snippet-store';
|
||||
import { storageErrorNotification } from './storage-errors';
|
||||
import { StorageQuotaError } from '../infrastructure/db';
|
||||
import { entityStorageErrorNotification, storageErrorNotification } from './storage-errors';
|
||||
|
||||
describe('storageErrorNotification', () => {
|
||||
describe('a failure the user can fix (storage full)', () => {
|
||||
@@ -43,3 +43,43 @@ describe('storageErrorNotification', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('entityStorageErrorNotification', () => {
|
||||
it('names the entity in the title, message, and diagnostic', () => {
|
||||
const save = entityStorageErrorNotification('dataset', 'save', new Error('boom'));
|
||||
expect(save.kind).toBe('error');
|
||||
expect(save.title).toBe("Couldn't save the dataset");
|
||||
expect(save.message).toContain('dataset change');
|
||||
expect(save.detail).toBe('Dataset save failed: Error: boom'); // capitalized noun in trace
|
||||
|
||||
const del = entityStorageErrorNotification('font', 'delete', new Error('boom'));
|
||||
expect(del.title).toBe("Couldn't delete the font");
|
||||
expect(del.detail).toContain('Font delete failed');
|
||||
});
|
||||
|
||||
it('detects a quota failure for any entity tier (not just snippets)', () => {
|
||||
const n = entityStorageErrorNotification('dataset', 'save', new StorageQuotaError());
|
||||
expect(n.title).toBe('Storage full');
|
||||
expect(n.message.toLowerCase()).toContain('dataset');
|
||||
expect(n.message.toLowerCase()).toContain('delete');
|
||||
expect(n.detail).toBeUndefined(); // nothing to report — the fix is in the message
|
||||
});
|
||||
|
||||
it('frames a load failure with the entity noun, not "snippets"', () => {
|
||||
const n = entityStorageErrorNotification(
|
||||
'dataset',
|
||||
'load',
|
||||
new DOMException('blocked', 'SecurityError'),
|
||||
);
|
||||
expect(n.title).toBe("Couldn't open your datasets");
|
||||
expect(n.message).toContain('datasets');
|
||||
expect(n.message).not.toContain('snippet');
|
||||
expect(n.detail).toContain('load');
|
||||
expect(n.detail).toContain('SecurityError');
|
||||
});
|
||||
|
||||
it('describes a non-Error throw without crashing', () => {
|
||||
const n = entityStorageErrorNotification('theme', 'save', 'weird string');
|
||||
expect(n.detail).toBe('weird string');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* that failed + the underlying error) so it can be reported and traced.
|
||||
*/
|
||||
|
||||
import { StorageQuotaError } from '../infrastructure/snippet-store';
|
||||
import { StorageQuotaError } from '../infrastructure/db';
|
||||
import type { NotifyOptions } from '../stores/NotificationStore';
|
||||
|
||||
/** Which persistence operation failed — shapes both the wording and the trace. */
|
||||
@@ -73,3 +73,58 @@ export function storageErrorNotification(op: StorageOp, err: unknown): NotifyOpt
|
||||
detail: diagnostic(op, err),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The storage-failure notification for the per-record entity tiers other than
|
||||
* snippets (datasets, themes, fonts), parameterized by entity noun. Mirrors the
|
||||
* snippet-flavored `storageErrorNotification` above — quota detection, the startup
|
||||
* `load` failure, and the generic save/delete failure — but with the entity's own
|
||||
* noun so a dataset error never reads "snippet". Snippets keep the bespoke function
|
||||
* above (its "your library" / "your changes" framing is tuned to the silent
|
||||
* save-on-edit model). `noun` is the lowercase singular ("dataset").
|
||||
*/
|
||||
export function entityStorageErrorNotification(
|
||||
noun: string,
|
||||
op: StorageOp,
|
||||
err: unknown,
|
||||
): NotifyOptions {
|
||||
const Noun = noun.charAt(0).toUpperCase() + noun.slice(1);
|
||||
const plural = `${noun}s`;
|
||||
const detail =
|
||||
err instanceof Error ? `${Noun} ${op} failed: ${err.name}: ${err.message}` : String(err);
|
||||
|
||||
// Storage full — the one failure the user can act on. No detail: the next step
|
||||
// is in the message. (Quota is whole-origin; per-tier framing is a parked UX
|
||||
// nuance — see docs/ux-second-pass.md.)
|
||||
if (err instanceof StorageQuotaError) {
|
||||
return {
|
||||
kind: 'error',
|
||||
title: 'Storage full',
|
||||
message:
|
||||
`This ${noun} couldn't be saved because ${noun} storage is full. ` +
|
||||
`Delete ${plural} you no longer need to free space, then try again.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Couldn't read this tier on startup — almost always blocked storage.
|
||||
if (op === 'load') {
|
||||
return {
|
||||
kind: 'error',
|
||||
title: `Couldn't open your ${plural}`,
|
||||
message:
|
||||
`Astrolabe couldn't open local storage, so your saved ${plural} aren't available yet. ` +
|
||||
'This usually means the browser is blocking storage — for example, private-browsing mode. ' +
|
||||
'Reload to try again.',
|
||||
detail,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'error',
|
||||
title: op === 'delete' ? `Couldn't delete the ${noun}` : `Couldn't save the ${noun}`,
|
||||
message:
|
||||
`A storage error stopped Astrolabe from completing the last ${noun} change, so it may not ` +
|
||||
'survive a reload. If this keeps happening, your browser may be blocking local storage.',
|
||||
detail,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ import { createCustomTheme } from '@core/custom-theme';
|
||||
import { createDataset } from '@core/dataset';
|
||||
import { createSnippet } from '@core/snippet';
|
||||
import { downloadJson, readTextFile } from '../infrastructure/file-transfer';
|
||||
import { deleteSnippet, saveSnippet, StorageQuotaError } from '../infrastructure/snippet-store';
|
||||
import { deleteSnippet, saveSnippet } from '../infrastructure/snippet-store';
|
||||
import { StorageQuotaError } from '../infrastructure/db';
|
||||
import { useCustomThemeStore } from '../stores/CustomThemeStore';
|
||||
import { useDatasetStore } from '../stores/DatasetStore';
|
||||
import { useNotificationStore } from '../stores/NotificationStore';
|
||||
|
||||
@@ -29,7 +29,8 @@ import {
|
||||
import { snippetSizeBytes } from '@core/snippet';
|
||||
import { humanizeBytes } from '@core/storage-estimate';
|
||||
import { downloadJson, readTextFile } from '../infrastructure/file-transfer';
|
||||
import { deleteSnippet, saveSnippet, StorageQuotaError } from '../infrastructure/snippet-store';
|
||||
import { deleteSnippet, saveSnippet } from '../infrastructure/snippet-store';
|
||||
import { StorageQuotaError } from '../infrastructure/db';
|
||||
import { notify } from '../stores/NotificationStore';
|
||||
import { useCustomThemeStore } from '../stores/CustomThemeStore';
|
||||
import { useDatasetStore } from '../stores/DatasetStore';
|
||||
|
||||
Reference in New Issue
Block a user