Skip to content
Merged
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
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Breaking changes are always listed first in each release section.

### Breaking changes

- None. The Nitro Modules `0.37.x` native rebuild requirement remains from
0.9.0; this release restores the previous set-item type and secure-write
defaults for existing consumers.
- Breaking changes: None. The Nitro Modules `0.37.x` native rebuild
requirement remains from 0.9.0; this release restores the previous set-item
type and secure-write defaults for existing consumers.

### Added

Expand All @@ -28,6 +28,8 @@ Breaking changes are always listed first in each release section.
`apply()` writes remain available through the explicit
`storage.setSecureWritesAsync(true)` opt-in and can be drained with
`storage.flushSecureWrites()`.
- Enabled raw read-cache lookups now reuse cached missing values in single and
batch reads without per-item fallback calls.

### Deprecated

Expand All @@ -41,6 +43,7 @@ Breaking changes are always listed first in each release section.
- Documented secure-storage recovery semantics and warned against cached
fallback for authentication tokens unless stale credentials are an explicit
application policy.
- Clarified isolated web benchmark limits and corrected capability/API examples.

## [0.9.0] - 2026-08-20

Expand Down
40 changes: 24 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,9 @@ can also throw when a protected store is locked or its key is invalidated. Use
matching item. `setBatch()` validates every item/value pair independently,
including heterogeneous batches.
Missing keys use each item's `defaultValue`; the native bridge preserves missing
entries as `undefined` while reading the batch.
entries as `undefined` while reading the batch. With `readCache: true`, item and
batch reads reuse raw cache entries, including cached missing values, until a
write, delete, clear, or external change invalidates the entry.

```ts
import { getBatch, removeBatch, setBatch } from "react-native-nitro-storage";
Expand Down Expand Up @@ -592,6 +594,11 @@ setWebDiskStorageBackend(backend);
setWebSecureStorageBackend(backend);
```

Web reads and mutations stay synchronous against the backend's in-memory
contract; use `flushWebStorageBackends()` for asynchronous persistence
boundaries. The native entry keeps the web backend setters, getters, and flush
function as typed no-ops for cross-platform code.

Browser storage cannot provide iOS Keychain or Android Keystore guarantees. Web
Secure scope is only as strong as the backend you configure.

Expand Down Expand Up @@ -622,28 +629,25 @@ const { storage, memoryItem } = createNitroStorageMock();

## API

The default export is a configured `storage` instance; `createStorage()`
builds isolated instances. Values are read and written through typed storage
items (`stringItem`, `numberItem`, `booleanItem`, `jsonItem`, plus custom
`createStorageItem` schemas) bound to a scope (`Memory`, `Disk`, or
`Secure`). The surface covers single-key operations (`get`/`set`/`remove`/
`has`), batch reads and writes, prefixed key enumeration, size queries,
`flushSecureWrites()`, clear-by-scope, events and observers, React hooks,
transactional migrations with rename/rollback, and the web backend adapter
API. The full reference lives in
The package exposes named `storage`, `createStorageItem`, the scoped item
factories, `createSetItem`, batch operations, migration and transaction helpers,
secure-auth storage, React hooks, and web backend utilities. Values are bound to
`Memory`, `Disk`, or `Secure` and support typed single-key operations, raw
inspection, events and observers, cache and write-flush controls, secure
metadata, transactional migrations with rename/rollback, and configurable web
backends. The full reference lives in
[docs/api-reference.md](docs/api-reference.md).

## Error Contract

Native failures cross the bridge as tagged, deterministic errors and surface
as typed `StorageError` values with stable string codes — identical codes on
iOS, Android, and web. Use `isStorageError(error, code)` to branch on them:
Native and web adapters tag classified failures with stable error codes. Use
`getStorageErrorCode(error)` or `isStorageError(error, code)` to branch on them:
`keychain_locked` reports a locked Keychain that a retry can recover after
authentication, secure-scope write or biometric failures carry their own
codes, and invalid inputs (bad scope, malformed keys, numeric guard
violations) are rejected before reaching native storage. Errors never
swallow the underlying cause silently: the original platform message is
preserved on the error for diagnostics.
violations) are rejected before reaching native storage. Errors never swallow
the underlying cause silently: the original platform message is preserved on
the error for diagnostics.

## Platform Support

Expand Down Expand Up @@ -702,6 +706,10 @@ Run native example builds before release when changing plugin, native, Nitro,
secure storage, or packaging files. The package release path also validates
package contents and dry-run publish behavior.

`bun run benchmark` measures only the built web entry with an isolated private
localStorage implementation; it is not a native Disk or Secure benchmark. See
[docs/benchmarks.md](docs/benchmarks.md) for sampling and interpretation limits.

## License

[MIT](LICENSE)
32 changes: 17 additions & 15 deletions apps/example/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useState } from "react";
import { memo, useRef, useState } from "react";
import { Platform, StyleSheet, Text, View } from "react-native";
import {
createSecureAuthStorage,
Expand Down Expand Up @@ -38,6 +38,11 @@ import {
} from "../components/shared";
import { SmokeTestRunner } from "../components/smoke-test";

const MemoizedAdvancedApiDemo = memo(AdvancedApiDemo);
const MemoizedErgonomicsDemo = memo(ErgonomicsDemo);
const MemoizedKeychainLifecycleProbe = memo(KeychainLifecycleProbe);
const MemoizedSmokeTestRunner = memo(SmokeTestRunner);

const counterItem = createStorageItem({
key: "counter",
scope: StorageScope.Memory,
Expand Down Expand Up @@ -282,7 +287,7 @@ function runRuntimeBenchmark() {
}
}

function RuntimeBenchmarkCard() {
const RuntimeBenchmarkCard = memo(function RuntimeBenchmarkCard() {
const [runtimeBenchmarkResult, setRuntimeBenchmarkResult] =
useState("(not run)");

Expand Down Expand Up @@ -310,12 +315,13 @@ function RuntimeBenchmarkCard() {
</CodeBlock>
</Card>
);
}
});

export default function HomeScreen() {
const [counter, setCounter] = useStorage(counterItem);

const [diskName, setDiskName] = useStorage(diskNameItem);
const hasDiskName = diskNameItem.has();
const [tempDiskName, setTempDiskName] = useState("");
const tempDiskNameRef = useRef("");

Expand Down Expand Up @@ -369,12 +375,8 @@ export default function HomeScreen() {
storage.size(StorageScope.Memory),
);

const [scopeDiskSize, setScopeDiskSize] = useState(() =>
storage.size(StorageScope.Disk),
);
const [scopeMemorySize, setScopeMemorySize] = useState(() =>
storage.size(StorageScope.Memory),
);
const [scopeDiskSize, setScopeDiskSize] = useState(diskSize);
const [scopeMemorySize, setScopeMemorySize] = useState(memorySize);

const [rawValue, setRawValue] = useState<string | undefined>();

Expand Down Expand Up @@ -448,12 +450,12 @@ export default function HomeScreen() {

return (
<Page title="Nitro Storage" subtitle="Complete feature showcase">
<SmokeTestRunner />
<MemoizedSmokeTestRunner />

<KeychainLifecycleProbe />
<MemoizedKeychainLifecycleProbe />

<ErgonomicsDemo />
<AdvancedApiDemo />
<MemoizedErgonomicsDemo />
<MemoizedAdvancedApiDemo />

<Card
title="Memory Scope"
Expand Down Expand Up @@ -673,8 +675,8 @@ export default function HomeScreen() {
<StatusRow
testID="disk-has-value"
label="has()"
value={String(diskNameItem.has())}
color={diskNameItem.has() ? Colors.success : Colors.muted}
value={String(hasDiskName)}
color={hasDiskName ? Colors.success : Colors.muted}
/>
</Card>

Expand Down
100 changes: 40 additions & 60 deletions apps/example/components/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,47 @@ export const Page = ({
);
};

const consumerStyles = {
row: {
flexDirection: "row",
alignItems: "center",
gap: 10,
},
flex1: {
flex: 1,
},
panel: {
backgroundColor: Colors.card,
borderWidth: 1,
borderColor: Colors.border,
borderRadius: 12,
padding: 12,
gap: 8,
},
panelTitle: {
color: Colors.muted,
fontFamily: fontSans700,
fontSize: 11,
textTransform: "uppercase",
letterSpacing: 0.8,
},
panelValue: {
color: Colors.text,
fontFamily: fontSans800,
fontSize: 42,
lineHeight: 44,
textAlign: "center",
},
helperText: {
color: Colors.muted,
fontFamily: fontSans400,
fontSize: 12,
lineHeight: 18,
},
} as const;

export const styles = StyleSheet.create({
...consumerStyles,
container: {
flex: 1,
backgroundColor: Colors.background,
Expand Down Expand Up @@ -552,12 +592,6 @@ export const styles = StyleSheet.create({
lineHeight: 18,
color: "#cbd5e1",
},
codeText: {
fontFamily: fontMono400,
fontSize: 12,
lineHeight: 18,
color: Colors.text,
},
section: {
gap: 10,
},
Expand All @@ -569,58 +603,4 @@ export const styles = StyleSheet.create({
letterSpacing: 1,
marginTop: 4,
},
row: {
flexDirection: "row",
alignItems: "center",
gap: 10,
},
grid: {
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap",
gap: 8,
},
flex1: {
flex: 1,
},
panel: {
backgroundColor: Colors.card,
borderWidth: 1,
borderColor: Colors.border,
borderRadius: 12,
padding: 12,
gap: 8,
},
panelTitle: {
color: Colors.muted,
fontFamily: fontSans700,
fontSize: 11,
textTransform: "uppercase",
letterSpacing: 0.8,
},
panelValue: {
color: Colors.text,
fontFamily: fontSans800,
fontSize: 42,
lineHeight: 44,
textAlign: "center",
},
helperText: {
color: Colors.muted,
fontFamily: fontSans400,
fontSize: 12,
lineHeight: 18,
},
});

const sharedStyleKeysForLint = [
styles.codeText,
styles.row,
styles.grid,
styles.flex1,
styles.panel,
styles.panelTitle,
styles.panelValue,
styles.helperText,
];
void sharedStyleKeysForLint;
Loading