@exodus/atoms
Advanced tools
Comparing version 8.1.1 to 9.0.0
@@ -6,2 +6,21 @@ # Change Log | ||
## [9.0.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/atoms@8.1.1...@exodus/atoms@9.0.0) (2024-09-26) | ||
### ⚠ BREAKING CHANGES | ||
- remove isSoleWriter from storage atom factory (#9423) | ||
### Features | ||
- don't allow get to proceed after unsubscribe ([#9441](https://github.com/ExodusMovement/exodus-hydra/issues/9441)) ([33bc642](https://github.com/ExodusMovement/exodus-hydra/commit/33bc642ad6ec32e1f31711f9dfe435d235eaca56)) | ||
- remove isSoleWriter from storage atom factory ([#9423](https://github.com/ExodusMovement/exodus-hydra/issues/9423)) ([ab90ee1](https://github.com/ExodusMovement/exodus-hydra/commit/ab90ee13a819058c0f23c37008da2bebf4439157)) | ||
### Bug Fixes | ||
- storage atom race condition ([#9403](https://github.com/ExodusMovement/exodus-hydra/issues/9403)) ([bf30d0c](https://github.com/ExodusMovement/exodus-hydra/commit/bf30d0cc90632459b6b0b9fd76fac191d20ddd0b)) | ||
### Performance Improvements | ||
- reduce underlying storage reads ([#9418](https://github.com/ExodusMovement/exodus-hydra/issues/9418)) ([f3c0c23](https://github.com/ExodusMovement/exodus-hydra/commit/f3c0c232b120977a27565636f4d1a6ca9fc90b4a)) | ||
## [8.1.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/atoms@8.1.0...@exodus/atoms@8.1.1) (2024-09-09) | ||
@@ -8,0 +27,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { Atom } from '../utils/types.js'; | ||
import type { Atom } from '../utils/types.js'; | ||
type Params<T> = { | ||
@@ -3,0 +3,0 @@ atom: Atom<T>; |
@@ -1,4 +0,4 @@ | ||
import { Atom } from './utils/types.js'; | ||
import type { Atom } from './utils/types.js'; | ||
type Params<T> = { | ||
makeGetConcurrent?: boolean; | ||
makeGetNonConcurrent?: boolean; | ||
getInitialized?: () => boolean; | ||
@@ -8,3 +8,3 @@ defaultValue?: T; | ||
} & Omit<Atom<T>, 'set' | 'reset'>; | ||
declare const enforceObservableRules: <T>({ defaultValue, makeGetConcurrent, getInitialized, ...atom }: Params<T>) => Atom<T>; | ||
declare const enforceObservableRules: <T>({ defaultValue, makeGetNonConcurrent, getInitialized, ...atom }: Params<T>) => Atom<T>; | ||
export default enforceObservableRules; |
@@ -17,10 +17,13 @@ import makeConcurrent from 'make-concurrent'; | ||
}; | ||
const enforceObservableRules = ({ defaultValue, makeGetConcurrent = false, getInitialized = () => true, ...atom }) => { | ||
const enforceObservableRules = ({ defaultValue, makeGetNonConcurrent = false, getInitialized = () => true, ...atom }) => { | ||
const enqueue = makeConcurrent((fn) => fn(), { concurrency: 1 }); | ||
const postProcessValue = (value = defaultValue) => value && typeof value === 'object' ? freeze(value) : value; | ||
const _get = makeGetConcurrent ? makeConcurrent(atom.get) : atom.get; | ||
const get = () => _get().then(postProcessValue); | ||
const nonConcurrentGet = makeGetNonConcurrent | ||
? makeConcurrent(atom.get) | ||
: atom.get; | ||
const get = () => nonConcurrentGet().then(postProcessValue); | ||
const observe = (listener) => { | ||
let called = false; | ||
let valueEmittedFromGet; | ||
let subscribed = true; | ||
listener = withChangeDetection(listener); | ||
@@ -31,3 +34,5 @@ const publishSerially = (value) => { | ||
}; | ||
atom.get().then((value) => { | ||
nonConcurrentGet().then((value) => { | ||
if (!subscribed) | ||
return; | ||
if (!called) { | ||
@@ -38,3 +43,3 @@ valueEmittedFromGet = value; | ||
}); | ||
return atom.observe((value) => { | ||
const unsubscribe = atom.observe((value) => { | ||
if (valueEmittedFromGet !== undefined) { | ||
@@ -49,2 +54,6 @@ const isAlreadyEmitted = value === valueEmittedFromGet; | ||
}); | ||
return () => { | ||
unsubscribe(); | ||
subscribed = false; | ||
}; | ||
}; | ||
@@ -51,0 +60,0 @@ const set = makeConcurrent(async (value) => { |
@@ -1,2 +0,2 @@ | ||
import { Atom, Listener } from '../utils/types.js'; | ||
import type { Atom, Listener } from '../utils/types.js'; | ||
type Params<T> = { | ||
@@ -3,0 +3,0 @@ atom: Atom<T>; |
@@ -1,2 +0,2 @@ | ||
import { Atom, ReadonlyAtom } from '../utils/types.js'; | ||
import type { Atom, ReadonlyAtom } from '../utils/types.js'; | ||
type CombinedValue<T> = { | ||
@@ -3,0 +3,0 @@ [Key in keyof T]: T[Key] extends Atom<infer U> ? U : never; |
@@ -1,2 +0,2 @@ | ||
import { Atom, ReadonlyAtom } from '../utils/types.js'; | ||
import type { Atom, ReadonlyAtom } from '../utils/types.js'; | ||
type Params<T, V> = { | ||
@@ -3,0 +3,0 @@ atom: Atom<T> | ReadonlyAtom<T>; |
@@ -1,3 +0,3 @@ | ||
import { Atom } from '../utils/types.js'; | ||
import type { Atom } from '../utils/types.js'; | ||
declare const dedupe: <T>(atom: Atom<T>) => Atom<T>; | ||
export default dedupe; |
@@ -1,2 +0,2 @@ | ||
import { Atom } from '../utils/types.js'; | ||
import type { Atom } from '../utils/types.js'; | ||
type Diff<T> = { | ||
@@ -3,0 +3,0 @@ previous?: T; |
@@ -1,2 +0,2 @@ | ||
import { Atom } from '../utils/types.js'; | ||
import type { Atom } from '../utils/types.js'; | ||
export default function filter<T>(atom: Atom<T>, predicate: (value: T) => boolean): Atom<T>; |
@@ -1,2 +0,2 @@ | ||
import { Atom } from '../utils/types.js'; | ||
import type { Atom } from '../utils/types.js'; | ||
type Params<T extends object> = { | ||
@@ -3,0 +3,0 @@ atom: Atom<T>; |
@@ -1,3 +0,3 @@ | ||
import { Atom } from '../utils/types.js'; | ||
import type { Atom } from '../utils/types.js'; | ||
declare const optimisticNotifier: <T>(atom: Atom<T>) => Atom<T | undefined>; | ||
export default optimisticNotifier; |
@@ -1,3 +0,3 @@ | ||
import { Atom, ReadonlyAtom } from '../utils/types.js'; | ||
import type { Atom, ReadonlyAtom } from '../utils/types.js'; | ||
declare const readOnly: <T>(atom: Atom<T>) => ReadonlyAtom<T>; | ||
export default readOnly; |
@@ -1,2 +0,2 @@ | ||
import { Atom, Listener, Logger } from '../utils/types.js'; | ||
import type { Atom, Listener, Logger } from '../utils/types.js'; | ||
type Params<T> = { | ||
@@ -3,0 +3,0 @@ atom: Atom<T>; |
@@ -1,2 +0,2 @@ | ||
import { Atom } from '../utils/types.js'; | ||
import type { Atom } from '../utils/types.js'; | ||
type Params<T> = { | ||
@@ -3,0 +3,0 @@ atom: Atom<T>; |
@@ -1,2 +0,2 @@ | ||
import { Atom, Logger } from '../utils/types.js'; | ||
import type { Atom, Logger } from '../utils/types.js'; | ||
type Params<T> = { | ||
@@ -3,0 +3,0 @@ atom: Atom<T>; |
@@ -1,2 +0,2 @@ | ||
import { Atom } from '../utils/types.js'; | ||
import type { Atom } from '../utils/types.js'; | ||
type Params<T, S> = { | ||
@@ -3,0 +3,0 @@ atom: Atom<S>; |
@@ -1,3 +0,3 @@ | ||
import { Atom } from '../utils/types.js'; | ||
import { Storage } from '@exodus/storage-interface'; | ||
import type { Atom } from '../utils/types.js'; | ||
import type { Storage } from '@exodus/storage-interface'; | ||
type Params<T> = { | ||
@@ -4,0 +4,0 @@ atom: Atom<T>; |
import createStorageAtomFactory from '../factories/storage.js'; | ||
const getCacheAtom = ({ storage, key }) => { | ||
const createStorageAtom = createStorageAtomFactory({ storage }); | ||
return createStorageAtom({ key, isSoleWriter: true }); | ||
return createStorageAtom({ key }); | ||
}; | ||
@@ -6,0 +6,0 @@ const enhanceAtom = (params) => { |
@@ -1,2 +0,2 @@ | ||
import { EventEmitter } from './utils/types.js'; | ||
import type { EventEmitter } from './utils/types.js'; | ||
type Params<T> = { | ||
@@ -3,0 +3,0 @@ emitter: EventEmitter; |
@@ -1,2 +0,2 @@ | ||
import { Atom, Keystore, KeystoreValue } from '../utils/types.js'; | ||
import type { Atom, Keystore, KeystoreValue } from '../utils/types.js'; | ||
type Params = { | ||
@@ -3,0 +3,0 @@ keystore: Keystore; |
@@ -1,2 +0,2 @@ | ||
import { Atom } from '../utils/types.js'; | ||
import type { Atom } from '../utils/types.js'; | ||
type Params<T> = { | ||
@@ -3,0 +3,0 @@ defaultValue?: T; |
@@ -1,2 +0,2 @@ | ||
import { ReadonlyAtom, Port } from '../utils/types.js'; | ||
import type { ReadonlyAtom, Port } from '../utils/types.js'; | ||
type Params<T> = { | ||
@@ -3,0 +3,0 @@ atom: ReadonlyAtom<T>; |
@@ -1,2 +0,2 @@ | ||
import { Keystore, KeystoreValue } from '../utils/types.js'; | ||
import type { Keystore, KeystoreValue } from '../utils/types.js'; | ||
type Params = { | ||
@@ -3,0 +3,0 @@ keystore: Keystore; |
@@ -1,3 +0,3 @@ | ||
import { Atom } from '../utils/types.js'; | ||
import { Storage } from '@exodus/storage-interface'; | ||
import type { Atom } from '../utils/types.js'; | ||
import type { Storage } from '@exodus/storage-interface'; | ||
type FactoryParams<T> = { | ||
@@ -9,3 +9,2 @@ storage: Storage<T>; | ||
defaultValue?: D; | ||
isSoleWriter?: boolean; | ||
}; | ||
@@ -12,0 +11,0 @@ declare const createStorageAtomFactory: <T>({ storage }: FactoryParams<T>) => { |
import createSimpleObserver from '../simple-observer.js'; | ||
import enforceObservableRules from '../enforce-rules.js'; | ||
import pDefer from 'p-defer'; | ||
const createStorageAtomFactory = ({ storage }) => { | ||
function createStorageAtom({ key, defaultValue, isSoleWriter, }) { | ||
const { notify, observe } = createSimpleObserver({ enable: isSoleWriter }); | ||
function createStorageAtom({ key, defaultValue }) { | ||
let version = 0; | ||
const { notify, observe } = createSimpleObserver({ enable: true }); | ||
let canUseCached = false; | ||
let cached; | ||
let writePromiseDefer; | ||
let pendingWrite; | ||
const set = async (value) => { | ||
if (value === undefined) { | ||
writePromiseDefer = pDefer(); | ||
await storage.delete(key); | ||
} | ||
else { | ||
writePromiseDefer = pDefer(); | ||
await storage.set(key, value); | ||
} | ||
writePromiseDefer.resolve(); | ||
if (isSoleWriter) { | ||
version++; | ||
pendingWrite = (async () => { | ||
if (value === undefined) { | ||
await storage.delete(key); | ||
canUseCached = false; | ||
} | ||
else { | ||
await storage.set(key, value); | ||
canUseCached = true; | ||
} | ||
cached = value; | ||
canUseCached = value !== undefined; | ||
await notify(value); | ||
} | ||
pendingWrite = undefined; | ||
})(); | ||
await pendingWrite; | ||
await notify(value); | ||
}; | ||
const get = async () => { | ||
if (canUseCached) { | ||
return cached; | ||
if (pendingWrite) { | ||
await pendingWrite; | ||
} | ||
if (writePromiseDefer) { | ||
await writePromiseDefer.promise; | ||
} | ||
if (canUseCached) { | ||
return cached; | ||
} | ||
const currentVersion = version; | ||
const value = await storage.get(key); | ||
if (isSoleWriter) { | ||
cached = value; | ||
canUseCached = true; | ||
if (currentVersion !== version) { | ||
return get(); | ||
} | ||
cached = value; | ||
canUseCached = true; | ||
return value; | ||
@@ -48,3 +48,3 @@ }; | ||
defaultValue, | ||
makeGetConcurrent: isSoleWriter, | ||
makeGetNonConcurrent: true, | ||
}); | ||
@@ -51,0 +51,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { Listener } from './utils/types.js'; | ||
import type { Listener } from './utils/types.js'; | ||
declare const createSimpleObserver: <T>({ enable }?: { | ||
@@ -3,0 +3,0 @@ enable?: boolean | undefined; |
@@ -1,2 +0,2 @@ | ||
import { Setter } from './types.js'; | ||
import type { Setter } from './types.js'; | ||
export declare function isSetter<T>(value: T | Setter<T>): value is Setter<T>; |
{ | ||
"name": "@exodus/atoms", | ||
"version": "8.1.1", | ||
"version": "9.0.0", | ||
"description": "Abstraction for encapsulating a piece of data behind a simple unified interface: get, set, observe", | ||
@@ -43,3 +43,4 @@ "type": "module", | ||
"@exodus/atom-tests": "^1.0.0", | ||
"@exodus/storage-memory": "^2.2.0", | ||
"@exodus/storage-encrypted": "^1.4.1", | ||
"@exodus/storage-memory": "^2.2.1", | ||
"@types/jest": "^29.5.11", | ||
@@ -51,3 +52,3 @@ "@types/json-stringify-safe": "^5.0.3", | ||
}, | ||
"gitHead": "d64de7579b06afe91fbded3d1f7eb29950f82e5a" | ||
"gitHead": "bbcb4c47a53d1770a36213ba77fa1f46bccc8d64" | ||
} |
71259
1073
8