Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/docs/stdlib-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ removing them. Store names, entry keys, values, serialized records, store and
entry counts, browser-key scans, and the aggregate UTF-16/UTF-8 payload are all
bounded. Truncation and corruption are reported as snapshot metadata so an
inspector cannot wedge application persistence or perform unbounded work.
When `PersistentStore` is loaded in a debug runtime, this snapshot is published
to the generic debugger Data panel as the read-only `persistent-store`
provider. The adapter applies a smaller 43 KiB transport projection and reports
the exact known store and entry omissions. Native bindings that do not expose
the snapshot callback advertise the provider as unavailable.

## Installation

Expand Down Expand Up @@ -445,4 +450,3 @@ The `persistence` module works on:
- **Encryption has overhead** - only use for sensitive data
- **LRU caching helps** - use maxWeight to limit storage usage
- **TTL prevents bloat** - set reasonable expiration times

23 changes: 11 additions & 12 deletions npm_modules/cli/debugger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,11 @@ Web-renderer inspection uses the first-party bridge exposed as
configured loopback Chromium target.

The Data section discovers target-owned providers through a generic custom
message contract. This slice includes read-only Storage and SQL presentation,
but it does not register either backend. A later thin registration layer can
adapt a bounded Storage snapshot callback or an existing SQL API without
changing this contract. Until then, the UI reports both surfaces as unavailable
instead of synthesizing sample data. Network and key-value integrations are not
part of this slice and remain unavailable unless a future runtime provider is
registered.
message contract. The persistence module registers its bounded web snapshot as
the `persistent-store` Storage provider and reports it unavailable on platforms
whose native binding does not expose that inspector. SQL, Network, and
key-value integrations remain unavailable unless a runtime provider registers
them; the UI never synthesizes sample data.

Runtime adapters cross the provider boundary with an already serialized JSON
object document, not an arbitrary object graph. They should call
Expand All @@ -82,11 +80,12 @@ the complete serialized custom-response body including metadata. Owners bind
to the creating module object's hot-reload callback automatically, including
webpack modules where `module.path` is absent. Adapters call
`owner.dispose()` only when stopping before a reload. A newer registration for
an existing provider ID permanently invalidates the older registration. This
is the integration point for the later thin Storage/SQL registration layer.
On native runtimes the owner observes `module.path`; the explicit stable key is
replacement identity only. Web runtimes fall back to observing that stable key
because webpack does not provide `module.path`.
an existing provider ID permanently invalidates the older registration. The
PersistentStore adapter projects known data properties into a deterministic
43 KiB document before calling this helper. On native runtimes the owner
observes `module.path`; the explicit stable key is replacement identity only.
Web runtimes fall back to observing that stable key because webpack does not
provide `module.path`.

Published settings are application-owned controls registered only in debug
runtimes. Values are limited to declared toggle, select, text, and number
Expand Down
26 changes: 22 additions & 4 deletions npm_modules/cli/debugger/debugger-providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,23 +248,41 @@ function renderProviderStatus(provider, unavailableMessage) {

function renderStorageEntry(entry) {
const truncated = entry.valueTruncated || entry.keyTruncated;
const metadata = [entry.encoding || 'unknown'];
const metadata = [entry.encoding ?? 'unknown'];
if (entry.byteLength !== undefined) metadata.push(`${entry.byteLength} bytes`);
if (truncated) metadata.push('truncated');
return `<details class="data-entry"><summary><code>${escapeHtml(entry.key || '')}</code><span>${escapeHtml(metadata.join(' · '))}</span></summary><pre class="codebox">${escapeHtml(entry.value ?? '')}</pre></details>`;
}

function renderStorageProjectionNote(storage) {
const projection = storage?.projection;
if (projection?.truncated !== true) return '';
const details = [];
const storesOmitted = Number(projection.storesOmitted);
const entriesOmitted = Number(projection.entriesOmitted);
const truncatedFields = Number(projection.truncatedFields);
const invalidFields = Number(projection.invalidFields);
if (Number.isInteger(storesOmitted) && storesOmitted > 0) details.push(`${storesOmitted} ${storesOmitted === 1 ? 'store' : 'stores'} omitted`);
if (Number.isInteger(entriesOmitted) && entriesOmitted > 0) details.push(`${entriesOmitted} ${entriesOmitted === 1 ? 'entry' : 'entries'} omitted`);
if (projection.sourceEntryCountIncomplete === true) details.push('source entry total incomplete');
if (Number.isInteger(truncatedFields) && truncatedFields > 0) details.push(`${truncatedFields} ${truncatedFields === 1 ? 'field' : 'fields'} truncated`);
if (Number.isInteger(invalidFields) && invalidFields > 0) details.push(`${invalidFields} invalid ${invalidFields === 1 ? 'field' : 'fields'} omitted`);
const detail = details.length ? `: ${details.join(', ')}` : '';
return `<div class="provider-note">The Storage transport projection was truncated${escapeHtml(detail)}.</div>`;
}

function renderStoragePanel() {
const provider = debuggerProviderForKind('storage');
const unavailable = debuggerProviderUnavailableMessage('storage', 'Storage');
if (!provider || provider.available !== true || !state.providers.storage) {
return `${renderProviderStatus(provider, unavailable)}<div class="empty">${escapeHtml(unavailable)}</div>`;
}
const stores = Array.isArray(state.providers.storage.stores) ? state.providers.storage.stores : [];
if (!stores.length) return `${renderProviderStatus(provider, unavailable)}<div class="empty">The registered Storage provider returned no stores.</div>`;
return `${renderProviderStatus(provider, unavailable)}<div class="storage-grid">${stores.map(store => {
const storageIssues = `${state.providers.storage.storageError ? `<div class="issue warn"><div class="issue-message">${escapeHtml(state.providers.storage.storageError)}</div></div>` : ''}${state.providers.storage.storageInspectionTruncated ? '<div class="provider-note">Persistent browser-storage discovery was truncated by the inspection budget.</div>' : ''}${renderStorageProjectionNote(state.providers.storage)}`;
if (!stores.length) return `${renderProviderStatus(provider, unavailable)}${storageIssues}<div class="empty">The registered Storage provider returned no stores.</div>`;
return `${renderProviderStatus(provider, unavailable)}${storageIssues}<div class="storage-grid">${stores.map(store => {
const entries = Array.isArray(store.entries) ? store.entries : [];
return `<details class="storage-card" open><summary><strong>${escapeHtml(store.name || 'Storage')}</strong><span>${escapeHtml(store.scope || 'unknown')} · ${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}</span></summary>${store.error ? `<div class="issue warn"><div class="issue-message">${escapeHtml(store.error)}</div></div>` : ''}${entries.length ? entries.map(renderStorageEntry).join('') : '<div class="empty">This store is empty.</div>'}${store.entriesTruncated ? '<div class="provider-note">Additional entries were omitted by the debugger snapshot budget.</div>' : ''}</details>`;
return `<details class="storage-card" open><summary><strong>${escapeHtml(store.name || 'Storage')}</strong><span>${escapeHtml(store.backend ?? 'unknown')} · ${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}</span></summary>${store.error ? `<div class="issue warn"><div class="issue-message">${escapeHtml(store.error)}</div></div>` : ''}${entries.length ? entries.map(renderStorageEntry).join('') : '<div class="empty">This store is empty.</div>'}${store.entriesTruncated ? '<div class="provider-note">Additional entries were omitted by the debugger snapshot budget.</div>' : ''}${store.inspectionTruncated ? '<div class="provider-note">Entry inspection stopped at the debugger scan limit.</div>' : ''}</details>`;
}).join('')}</div>`;
}

Expand Down
66 changes: 66 additions & 0 deletions npm_modules/cli/src/debugger/browserTools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,70 @@ describe('debugger browser provider and settings tools', () => {
'SQL support is not registered by this target runtime.',
);
});

it('renders bounded PersistentStore diagnostics without losing numeric string encoding', () => {
const harness = createHarness();
const state = harness.context['state'] as {
providers: { registry: Record<string, unknown>; storage: Record<string, unknown> };
};
state.providers.registry = {
providers: [{ available: true, id: 'persistent-store', kind: 'storage', label: 'PersistentStore' }],
};
state.providers.storage = {
storageError: 'Storage access was denied.',
storageInspectionTruncated: true,
stores: [
{
backend: 'memory',
entries: [{ encoding: 0, key: 'theme', value: 'dark' }],
inspectionTruncated: true,
name: 'preferences',
},
],
};

const html = call<string>(harness.context, 'renderStoragePanel');
expect(html).toContain('Storage access was denied.');
expect(html).toContain('Persistent browser-storage discovery was truncated');
expect(html).toContain('memory · 1 entry');
expect(html).toContain('0</span>');
expect(html).toContain('Entry inspection stopped at the debugger scan limit.');
expect(html).not.toContain('unknown · 1 entry');
});

it('surfaces provider projection omissions globally and on the affected store', () => {
const harness = createHarness();
const state = harness.context['state'] as {
providers: { registry: Record<string, unknown>; storage: Record<string, unknown> };
};
state.providers.registry = {
providers: [{ available: true, id: 'persistent-store', kind: 'storage', label: 'PersistentStore' }],
};
state.providers.storage = {
projection: {
entriesOmitted: 60,
invalidFields: 0,
sourceEntries: 100,
sourceStores: 2,
storesOmitted: 1,
truncated: true,
truncatedFields: 1,
},
stores: [
{
backend: 'memory',
entries: [{ encoding: 0, key: 'first', value: 'value' }],
entriesTruncated: true,
name: 'bounded',
},
],
};

const html = call<string>(harness.context, 'renderStoragePanel');
expect(html).toContain('Storage transport projection was truncated');
expect(html).toContain('1 store omitted');
expect(html).toContain('60 entries omitted');
expect(html).toContain('1 field truncated');
expect(html).toContain('Additional entries were omitted by the debugger snapshot budget.');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { toError } from 'valdi_core/src/utils/ErrorUtils';
import { makeSingleCallInterruptibleCallback } from 'valdi_core/src/utils/FunctionUtils';
import { PropertyList } from 'valdi_core/src/utils/PropertyList';
import { PersistentStoreNative } from './PersistentStoreNative';
import './PersistentStoreDebuggerProvider';

declare function require(path: string): any;
const nativeCreate: (
Expand Down
Loading
Loading