API
nsh/storage
nsh/storage keeps the app’s data on the device: strings by string key, still there the next time the app starts. Use it for settings, drafts, the last tab the user had open, a cache of what the server sent — any state that should outlive the app’s process.
// Storage: a launch counter kept across reloads, the stored keys and a reset.
import { mount, signal, Text, View } from "nsh";
import { Storage } from "nsh/storage";
import { Button } from "nsh/ui/mobile";
import { ui } from "./palette";
function App() {
const opened = signal<number | null>(null);
const keys = signal<string[]>([]);
const start = async () => {
const count = Number((await Storage.getItem("opened")) ?? "0") + 1;
await Storage.setItem("opened", String(count));
await Storage.setItem("lastOpened", new Date().toISOString());
opened.set(count);
keys.set(await Storage.getAllKeys());
};
const reset = async () => {
await Storage.clear();
opened.set(0);
keys.set(await Storage.getAllKeys());
};
start();
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 16, paddingTop: 24, gap: 12 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>Storage</Text>
<View style={{ borderRadius: 14, padding: 16, gap: 6, ...ui().card }}>
<Text style={{ fontSize: 28, fontWeight: "700", color: ui().text }}>
{opened() === null ? "Reading…" : `Opened ${opened()} times`}
</Text>
<Text style={{ color: ui().muted }}>Reload the page: the count is still there.</Text>
</View>
<Text style={{ color: ui().muted }}>{`Keys: ${keys().length ? keys().join(", ") : "none"}`}</Text>
<Button title="Reset" variant="secondary" onPress={reset} />
</View>
);
}
mount(App);import { Storage } from "nsh/storage";
const Storage: {
getItem(key: string): Promise<string | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
getAllKeys(): Promise<string[]>;
clear(): Promise<void>;
};
Each app has a storage of its own, up to 5 MB of keys and values. Calls run in the order the app makes them, so a getItem after a setItem sees the new value without waiting for the write. Reads come from memory; writes reach the disk in the background, a few at a time.
| Platform | Where the data is kept |
|---|---|
| iOS | Library/Application Support/nsh/<app id>/storage.json in the app’s container |
| Android | files/nsh/<app id>/storage.json in the app’s private directory |
| macOS | ~/Library/Application Support/<app id>/storage.json |
| Windows | %APPDATA%\<app id>\storage.json |
| Linux | $XDG_DATA_HOME/<app id>/storage.json (~/.local/share/…) |
| Web | The browser’s localStorage, under keys named after the app id (nsh:com.example.todo:…) |
A storage.json that is damaged is kept as storage.json.corrupt next to it, and the app starts with an empty storage; one that cannot be read leaves the app’s changes in memory, so the file is never replaced.
The app id is the id in nsh.json, and it travels with the app: nsh build and nsh package write it next to the bundle, the dev server sends it, and a signed package carries the one its release key’s certificate vouches for. So every app keeps its own data with no setting, wherever it runs; an app without an id keeps it under default. An embedding app can choose another place (see Embedding on iOS and Embedding on Android).
#Storage.getItem
getItem(key: string): Promise<string | null>;
Resolves with the value of key, or null when it has none.
const theme = (await Storage.getItem("theme")) ?? "system";
#Storage.setItem
setItem(key: string, value: string): Promise<void>;
Sets key to value, replacing what it held. Rejects when the storage would grow past 5 MB; the value is not kept then. Values are strings: keep structured data with JSON.stringify and read it back with JSON.parse.
Storage.setItem("draft", JSON.stringify({ title: title(), body: body() }));
#Storage.removeItem
removeItem(key: string): Promise<void>;
Removes key and its value. Removing a key that has no value does nothing.
#Storage.getAllKeys
getAllKeys(): Promise<string[]>;
Resolves with every key, in order.
#Storage.clear
clear(): Promise<void>;
Removes every key of the app, such as when the user signs out.
#Keeping a signal on the device
Read the stored value when the app starts, then write it back whenever the signal changes:
import { effect, mount, signal, Text, View } from "nsh";
import { Storage } from "nsh/storage";
const count = signal(0);
const loaded = signal(false);
Storage.getItem("count").then((saved) => {
count.set(Number(saved ?? 0));
loaded.set(true);
});
effect(() => {
if (loaded()) Storage.setItem("count", String(count()));
});
mount(() => (
<View onPress={() => count.update((n) => n + 1)}>
<Text>Pressed {count()} times</Text>
</View>
));
#From Rust
Rust components read and write the same storage with nsh_core::storage::Storage, whose functions mirror the TS ones, so an app with a TSX entry and a Rust entry sees one set of data. They are synchronous: the entries are in memory.
use nsh_core::storage::Storage;
let count: u32 = Storage::get_item("count").and_then(|n| n.parse().ok()).unwrap_or(0);
Storage::set_item("count", (count + 1).to_string())?;
| TS | Rust |
|---|---|
await Storage.getItem(key) | Storage::get_item(key) -> Option<String> |
await Storage.setItem(key, value) | Storage::set_item(key, value) -> Result<(), StorageFull> |
await Storage.removeItem(key) | Storage::remove_item(key) |
await Storage.getAllKeys() | Storage::get_all_keys() -> Vec<String> |
await Storage.clear() | Storage::clear() |