Changelog
All notable changes to this project will be documented in this file.
0.10.0 - 2026-10-28
This release makes imports safer, preserves legacy expiration deadlines, and adds optional persistence diagnostics and default write options to the existing React hooks.
Breaking changes
- Renamed the
getItem()optionlegacytomigrateFromand the exportedLegacyOptionstype toMigrateFromOptions. Passinglegacynow throws a descriptive error. - Import deserializers must synchronously return
{ value, options? }. Wrap every decoded value, including strings and objects.optionsreusesStorageOptions: useexpiresAtto preserve an absolute deadline orttlto start a new lifetime. Omitted options import without expiration. - Successful imports retain the source key instead of deleting it. The application owns verification, migration completion tracking, and source cleanup.
- Removed
getItemResult(). UsegetItem(key, { detailed: true })instead. The exportedReadOptionstype was removed; useGetOptions.
Improvements
- Detailed reads include
__result: trueand distinguish successful, missing, expired, unsupported, unparseable, and schema-invalid data. Verified imports additionally returnmigrated: true; ordinary reads still return a value ornull. - Imports preserve the supplied expiration, skip expired sources, and protect every occupied destination. Validated output is written and read back before an import is reported as successful. Source data is retained on decoding, validation, serialization, backend, and write failures, including during quota recovery.
- Added
writeOptionstouseStorage()andcreateStorageHook()for default expiration policies. Explicit setter expiration replaces the entire default policy;{ expiresAt: null }removes expiration. Reads, rerenders, and hydration never refresh deadlines. - Added opt-in
diagnostics: trueto the existing React hooks. A fourth tuple item exposesreadResultand the last write or remove error, including unavailable storage and unreadable data. Setters and removers still throw, and the default three-item tuple is unchanged. - Improved React recovery when storage becomes available again, subscription cleanup, and side-effect-free SSR and hydration.
- Updated the default serializer to devalue 6 and refreshed the documentation toolchain.
Upgrade notes
Replace standalone detailed reads:
// Before:
const result = appStorage.getItemResult('draft');
// After:
const result = appStorage.getItem('draft', { detailed: true });
Rename legacy to migrateFrom and wrap the decoded value. Keep legacy format decoding in your application. For an old JSON record containing { value, expiresAt }:
const result = appStorage.getItem('draft', {
detailed: true,
migrateFrom: {
key: 'old:draft',
deserialize(raw) {
const old = JSON.parse(raw);
return {
value: old.value,
options: { expiresAt: old.expiresAt ?? null },
};
},
},
});
if (result.status === 'success' && result.migrated) {
// The destination value and expiration were written and verified.
// The source is still retained for application verification and cleanup.
}
Use the original absolute expiresAt to preserve remaining lifetime. Never supply both ttl and expiresAt. Decoder exceptions and schema issues become read outcomes; malformed decoder results, invalid options, backend failures, and failed writes still throw.
Use a separate namespace when changing storage formats. Retained source keys can be imported again after a destination is deleted, so coordinate older clients and perform application-owned cleanup or track migration completion. See the migration guide.
React diagnostics expose failures without blocking writes or retaining an unsaved editor buffer. Draft interfaces should use readResult to decide when saving is safe, keep unsaved text in application state, and catch setter errors. See the React guide.
0.9.0 - 2026-09-16
- Breaking: Removed automatic JSON fallback after a serializer parser throws. Reads now use only the configured parser unless
fallbackParseris explicitly provided, including when using the defaultdevalueserializer. (#4) - Added
fallbackParserto bothcreateStorage()entry points. It runs only after a synchronous primary parser exception; if both parsers throw,getItemResult()retains the primary error. Promise or thenable output from either parser throws a synchronousTypeError. - Added opt-in
onQuotaExceeded: 'clear-expired'recovery. A backendQuotaExceededErrortriggers cleanup of recognized expired entries in the current namespace, followed by one retry of the same serialized write. Cleanup and retry failures propagate. - Fixed asynchronous custom parsers being silently treated as unsupported data and leaking rejected promises. Promise-like parser output now throws a synchronous
TypeError, with any rejection consumed. (#3) - Reorganized internal source into core and React modules while preserving the
ultrastorage,ultrastorage/core, andultrastorage/reactentry points. - Expanded migration, parser error, and quota recovery documentation, and refreshed the documentation site’s branding and navigation.
Upgrade notes
Add fallbackParser: JSON.parse to preserve the previous JSON compatibility behavior. Without it, JSON envelopes rejected by the configured parser remain stored but are unreadable, excluded from enumeration, and skipped by clear() and clearExpired(). Use a compatible parser to migrate them, or removeItem(key) to remove known obsolete entries. Writes continue to use serializer.stringify().
const appStorage = createStorage({
prefix: 'app',
fallbackParser: JSON.parse,
});
Keep your existing serializer, prefix, separator, and backend settings when adding the fallback. React hooks and reactive expiration follow the storage instance’s parsing policy. See the migration guide for details.
Custom parsers must be synchronous. A promise or thenable now throws instead of returning an unsupported result; changing the fallback configuration does not enable async parsing.
Quota recovery is disabled by default. Enabling it does not evict live entries or fall back to memory, and a write can still fail after cleanup and the single retry.
Contributors
Thanks to @rickvian for reporting the asynchronous-parser issue (#2), contributing the fix (#3), and providing the reproduction and proposal for explicit fallback parsing (#4).
0.8.0 - 2026-09-11
- Added
getItem()legacy imports with a custom deserializer and optional schema validation. Imports run only when the destination is physically missing, persist the validated value without expiration, and remove the legacy key after a successful write. - Added
getExpiration()to inspect stored deadlines without cleanup andsetExpiration()to change or remove expiration without supplying the value. - Changed writes without expiration options to preserve the current unexpired deadline, including
updateItem()and React setters. Pass{ expiresAt: null }to remove expiration explicitly. - Added
getItemResult()with distinct outcomes for successful reads, missing and expired entries, unsupported data, parse errors, and validation errors. Successful results distinguish storednullfrom a missing value. - Added opt-in
{ reactiveExpiration: true }for subscriptions and React hooks. Expiration timers notify listeners and update React values without deleting entries, with rechecks on window focus and page visibility changes. - Expanded documentation for migration, expiration controls, detailed reads, and reactive expiration, and added a changelog page.
Upgrade notes
- Previously, writing without expiration options removed an existing deadline. To retain that behavior, pass
{ expiresAt: null }tosetItem(),updateItem(), or a React setter. Pass a newttlto restart the lifetime instead of preserving the original deadline. - Reactive expiration remains opt-in. Timer notifications may be delayed, do not remove stored entries, and can be followed by another
expireevent during lazy cleanup. - Legacy imports are available through
getItem()only.getItemResult()does not import legacy data; backend failures, thrown schema errors, and unsupported asynchronous validation still throw.
0.7.0 - 2026-09-11
- Added array storage keys through the exported
StorageKeytype across storage methods, subscriptions, and React hooks. Equivalent arrays address the same entry without depending on array identity. - Added
keys()to enumerate non-expired keys in one scan. Bothkey()andkeys()restore array keys as fresh arrays of their original string segments. - Added development warnings when a namespace prefix contains its separator, indicating possible overlap with broader namespaces.
- Fixed package imports in browser environments without a global
process, with smoke checks for both ESM and CommonJS builds. - Hardened stored-entry validation and rejected non-finite expiration timestamps, including invalid dates and overflowing TTLs.
- Fixed Standard Schema handling for successful results with
issues: undefinedand asynchronous validators returning promises or thenables. Unsupported asynchronous validation now consumes rejections before throwing a synchronous error. - Improved in-memory key enumeration by caching ordered keys until insertions or deletions change them.
- Expanded documentation for React usage, namespace overlap, storage durability, migration, and synchronous helper behavior.
Upgrade notes
key()now returnsStorageKey | nullinstead ofstring | null. Update callers that assume every enumerated key is a string.UltraStorageno longer intersects the nativeStoragetype; useUltraStoragefor application instances.- Existing string keys and
__us/__gsentries remain supported. Avoid authoring string keys beginning with the reserved array-key prefix\u0000us:a:; canonical encoded strings address the same entries as their corresponding arrays. - Subscription events retain a string
change.key; array keys use their opaque serialized form. - Invalid or non-finite
ttlandexpiresAtvalues now throwTypeError. Stored envelopes must have a value, version1, and an expiry ofnullor a finite number to be recognized.
0.6.0 - 2026-09-10
-
Renamed the package from
greatstoragetoultrastorageand the exportedGreatStoragetype toUltraStorage. -
Added key-level subscriptions through
subscribe(key, listener), including same-page updates across instances sharing a backend and native cross-tab storage events. -
Made the
UltraStorageinterface a strict superset of the nativeStorageinterface. -
New entries use the internal storage marker
__us. Existing__gsentries remain supported without migration. -
Added optional React 18/19 bindings through
ultrastorage/react:useStorageandcreateStorageHook, with typed fallbacks, schema reads, functional updates, and expiration options. -
Added cached, side-effect-free internal snapshots and SSR/hydration support. Core reads retain their existing fresh-object and lazy-cleanup behavior.
-
Deferred default
localStorageaccess until the first operation, making instance construction safe on the server. Backend failures now occur on use rather than construction; no memory fallback is introduced. -
Added adapter rendering, hydration, subscription, failure, and type coverage.
vp run test:coverageenforces 100% runtime coverage. -
Added documentation at https://ultrastorage.dev.
Upgrade notes
- Replace the
greatstoragedependency and import paths withultrastorage, includinggreatstorage/corewithultrastorage/core. Rename type imports fromGreatStoragetoUltraStorage. - Existing
__gsentries remain readable. Older versions cannot read newly written__usentries, so update applications that share the same storage together. - React remains optional. Install React 18 or 19 only when using
ultrastorage/react. - Default storage is resolved on first use, so constructing an instance on the server is safe; operations still require an available storage backend.
0.5.0 - 2026-03-10
ultrastorage 0.5.0 focuses on better bundle control for custom serializers and safer development ergonomics.
Highlights
- Added a new
ultrastorage/coreentry point for applications that provide their own serializer and want to avoid bundlingdevalue. - Added development-only warnings for two easy-to-miss cases:
- writing
null, which reads back the same as a missing key viagetItem() - writing values with an expiry that is already in the past
- writing
- Expanded test coverage for the new core entry point and the warning behavior.
- Improved the README to document the new entry point and clarify the public API.
Why ultrastorage/core exists
The default ultrastorage entry point still includes devalue so rich JavaScript values work out of the box. If you already use a custom serializer such as superjson, ultrastorage/core lets you keep the same storage API while making the serializer explicit and avoiding the extra default serialization dependency in your bundle.
Upgrade notes
- No breaking changes in the default
ultrastorageentry point. ultrastorage/coreis new in this release. ItscreateStorage()requires aserializer.- The new warnings only run outside production builds and do not change runtime behavior.