@plq/use-persisted-state 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +165 -50
- package/esm/index.d.mts +13 -0
- package/esm/index.mjs +10 -0
- package/esm/storages/browser-storage.d.mts +1 -0
- package/esm/storages/browser-storage.mjs +5 -0
- package/esm/storages/chrome-storage.d.mts +1 -0
- package/esm/storages/chrome-storage.mjs +5 -0
- package/esm/storages/local-storage.d.mts +1 -0
- package/esm/storages/local-storage.mjs +3 -0
- package/esm/storages/session-storage.d.mts +1 -0
- package/esm/storages/session-storage.mjs +3 -0
- package/lib/@types/storage.d.ts.map +1 -1
- package/lib/create-async-persisted-state.d.ts +2 -2
- package/lib/create-async-persisted-state.d.ts.map +1 -1
- package/lib/create-async-persisted-state.js +62 -12
- package/lib/create-async-persisted-state.js.map +1 -1
- package/lib/create-persisted-state.d.ts +2 -2
- package/lib/create-persisted-state.d.ts.map +1 -1
- package/lib/create-persisted-state.js +31 -13
- package/lib/create-persisted-state.js.map +1 -1
- package/lib/index.d.ts +6 -5
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -2
- package/lib/index.js.map +1 -1
- package/lib/storages/browser-storage.d.ts +2 -2
- package/lib/storages/browser-storage.d.ts.map +1 -1
- package/lib/storages/browser-storage.js +6 -26
- package/lib/storages/browser-storage.js.map +1 -1
- package/lib/storages/chrome-storage.d.ts +2 -2
- package/lib/storages/chrome-storage.d.ts.map +1 -1
- package/lib/storages/chrome-storage.js +10 -27
- package/lib/storages/chrome-storage.js.map +1 -1
- package/lib/storages/local-storage.d.ts +1 -1
- package/lib/storages/local-storage.d.ts.map +1 -1
- package/lib/storages/local-storage.js +3 -1
- package/lib/storages/local-storage.js.map +1 -1
- package/lib/storages/session-storage.d.ts +1 -1
- package/lib/storages/session-storage.d.ts.map +1 -1
- package/lib/storages/session-storage.js +3 -1
- package/lib/storages/session-storage.js.map +1 -1
- package/lib/utils/change-notifier.d.ts +18 -0
- package/lib/utils/change-notifier.d.ts.map +1 -0
- package/lib/utils/change-notifier.js +38 -0
- package/lib/utils/change-notifier.js.map +1 -0
- package/lib/utils/create-web-storage.d.ts +10 -2
- package/lib/utils/create-web-storage.d.ts.map +1 -1
- package/lib/utils/create-web-storage.js +72 -75
- package/lib/utils/create-web-storage.js.map +1 -1
- package/lib/utils/extension-storage.d.ts +38 -0
- package/lib/utils/extension-storage.d.ts.map +1 -0
- package/lib/utils/extension-storage.js +66 -0
- package/lib/utils/extension-storage.js.map +1 -0
- package/lib/utils/get-new-item.d.ts.map +1 -1
- package/lib/utils/get-new-item.js.map +1 -1
- package/lib/utils/get-new-value.d.ts +1 -1
- package/lib/utils/get-new-value.d.ts.map +1 -1
- package/lib/utils/get-persisted-value.d.ts +8 -1
- package/lib/utils/get-persisted-value.d.ts.map +1 -1
- package/lib/utils/get-persisted-value.js +19 -5
- package/lib/utils/get-persisted-value.js.map +1 -1
- package/lib/utils/is-async-storage.d.ts +7 -0
- package/lib/utils/is-async-storage.d.ts.map +1 -1
- package/lib/utils/is-async-storage.js +51 -20
- package/lib/utils/is-async-storage.js.map +1 -1
- package/lib/utils/storage-event-router.d.ts +17 -0
- package/lib/utils/storage-event-router.d.ts.map +1 -0
- package/lib/utils/storage-event-router.js +54 -0
- package/lib/utils/storage-event-router.js.map +1 -0
- package/lib/utils/use-storage-handler.d.ts +3 -3
- package/lib/utils/use-storage-handler.d.ts.map +1 -1
- package/lib/utils/use-storage-handler.js +80 -26
- package/lib/utils/use-storage-handler.js.map +1 -1
- package/package.json +97 -41
- package/src/@types/storage.ts +13 -13
- package/src/create-async-persisted-state.ts +81 -18
- package/src/create-persisted-state.ts +48 -22
- package/src/index.ts +12 -7
- package/src/storages/browser-storage.ts +8 -36
- package/src/storages/chrome-storage.ts +24 -46
- package/src/storages/local-storage.ts +3 -1
- package/src/storages/session-storage.ts +3 -1
- package/src/utils/change-notifier.ts +43 -0
- package/src/utils/create-web-storage.ts +74 -77
- package/src/utils/extension-storage.ts +79 -0
- package/src/utils/get-new-item.ts +1 -1
- package/src/utils/get-new-value.ts +1 -1
- package/src/utils/get-persisted-value.ts +22 -7
- package/src/utils/is-async-storage.ts +72 -27
- package/src/utils/storage-event-router.ts +67 -0
- package/src/utils/use-storage-handler.ts +112 -36
- package/storages/browser-storage.d.ts +1 -0
- package/storages/browser-storage.js +11 -0
- package/storages/chrome-storage.d.ts +1 -0
- package/storages/chrome-storage.js +11 -0
- package/storages/local-storage.d.ts +1 -0
- package/storages/local-storage.js +9 -0
- package/storages/session-storage.d.ts +1 -0
- package/storages/session-storage.js +9 -0
package/src/@types/storage.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
export interface StorageChange {
|
|
2
|
-
oldValue?: string | null
|
|
3
|
-
newValue?: string | null
|
|
2
|
+
oldValue?: string | null
|
|
3
|
+
newValue?: string | null
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
export type StorageChangeListener = (changes: { [key: string]: StorageChange }) => void
|
|
7
7
|
|
|
8
8
|
export interface StorageChangeEvent {
|
|
9
|
-
addListener(listener: StorageChangeListener): void
|
|
10
|
-
removeListener(listener: StorageChangeListener): void
|
|
11
|
-
hasListener(listener: StorageChangeListener): boolean
|
|
9
|
+
addListener(listener: StorageChangeListener): void
|
|
10
|
+
removeListener(listener: StorageChangeListener): void
|
|
11
|
+
hasListener(listener: StorageChangeListener): boolean
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface Storage {
|
|
15
|
-
get: (keys: string | string[]) => { [key: string]: string }
|
|
16
|
-
set: (items: { [key: string]: string }) => void
|
|
17
|
-
remove: (keys: string | string[]) => void
|
|
18
|
-
onChanged: StorageChangeEvent
|
|
15
|
+
get: (keys: string | string[]) => { [key: string]: string }
|
|
16
|
+
set: (items: { [key: string]: string }) => void
|
|
17
|
+
remove: (keys: string | string[]) => void
|
|
18
|
+
onChanged: StorageChangeEvent
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface AsyncStorage {
|
|
22
|
-
get: (keys: string | string[]) => Promise<{ [key: string]: string }
|
|
23
|
-
set: (items: { [key: string]: string }) => Promise<void
|
|
24
|
-
remove: (keys: string | string[]) => Promise<void
|
|
25
|
-
onChanged: StorageChangeEvent
|
|
22
|
+
get: (keys: string | string[]) => Promise<{ [key: string]: string }>
|
|
23
|
+
set: (items: { [key: string]: string }) => Promise<void>
|
|
24
|
+
remove: (keys: string | string[]) => Promise<void>
|
|
25
|
+
onChanged: StorageChangeEvent
|
|
26
26
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
3
|
|
|
3
4
|
import useStorageHandler from './utils/use-storage-handler'
|
|
4
5
|
import getNewValue from './utils/get-new-value'
|
|
5
6
|
import getNewItem from './utils/get-new-item'
|
|
6
7
|
import getPersistedValue from './utils/get-persisted-value'
|
|
7
8
|
|
|
8
|
-
import {AsyncStorage} from './@types/storage'
|
|
9
|
-
import {PersistedState, UsePersistedState} from './@types/hook'
|
|
9
|
+
import type { AsyncStorage } from './@types/storage'
|
|
10
|
+
import type { PersistedState, UsePersistedState } from './@types/hook'
|
|
10
11
|
|
|
11
12
|
export default function createAsyncPersistedState<S extends AsyncStorage>(
|
|
12
13
|
storageKey: string,
|
|
@@ -21,30 +22,92 @@ export default function createAsyncPersistedState<S extends AsyncStorage>(
|
|
|
21
22
|
const usePersistedState = <T>(key: string, initialValue: T | (() => T)): UsePersistedState<T> => {
|
|
22
23
|
const [state, setState] = useState<T>(initialValue)
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
// The setter must resolve updater functions against the last value applied,
|
|
26
|
+
// not against the one captured by the render that created it. Reading the
|
|
27
|
+
// render closure collapses updates batched into a single event.
|
|
28
|
+
const latestValue = useRef(state)
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
// Whether anything has already put a value in place. A load that settles after
|
|
31
|
+
// the caller set one must not revert it, and no signal local to the load can
|
|
32
|
+
// answer this: the write it loses to happens outside the load entirely.
|
|
33
|
+
const hasAppliedValue = useRef(false)
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
const applyValue = useCallback((value: T): void => {
|
|
36
|
+
latestValue.current = value
|
|
37
|
+
hasAppliedValue.current = true
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
}
|
|
39
|
+
setState(value)
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
// The exact entry this hook last wrote and has not yet seen reported back.
|
|
43
|
+
const pendingOwnWrite = useRef<string | null>(null)
|
|
44
|
+
|
|
45
|
+
const setPersistedState = useCallback(
|
|
46
|
+
async (newState: React.SetStateAction<T>): Promise<void> => {
|
|
47
|
+
const newValue = getNewValue<T>(newState, latestValue.current)
|
|
48
|
+
|
|
49
|
+
applyValue(newValue)
|
|
50
|
+
|
|
51
|
+
const persistedItem = await storage.get(safeStorageKey)
|
|
52
|
+
const newItem = getNewItem<T>(key, persistedItem[safeStorageKey], newValue)
|
|
53
|
+
|
|
54
|
+
// Recorded before the write, because the backend may report it before the
|
|
55
|
+
// promise settles.
|
|
56
|
+
pendingOwnWrite.current = newItem
|
|
57
|
+
|
|
58
|
+
await storage.set({ [safeStorageKey]: newItem })
|
|
59
|
+
},
|
|
60
|
+
[key, applyValue],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// As in `useState`, the value the load falls back to is the one given on the
|
|
64
|
+
// first render. Later identities of an inline object must not reload, or the
|
|
65
|
+
// effect re-runs on every render and never settles.
|
|
66
|
+
const mountInitialValue = useRef(initialValue)
|
|
67
|
+
|
|
68
|
+
// Subscribed before the load below, and the order is load-bearing: React runs
|
|
69
|
+
// effects in declaration order, so reading first would leave an interval
|
|
70
|
+
// between the read and the subscription in which a write belongs to neither
|
|
71
|
+
// and is lost. Reading last covers everything written before the subscription
|
|
72
|
+
// began, and the listener covers everything after.
|
|
73
|
+
useStorageHandler<T>(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrite)
|
|
34
74
|
|
|
35
75
|
useEffect(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
76
|
+
// Two separate questions, and one cell cannot hold both: whether this load
|
|
77
|
+
// is still the current one, which only the closure it belongs to can answer,
|
|
78
|
+
// and whether a value has been applied meanwhile, which outlives it. A load
|
|
79
|
+
// abandoned by a key change would otherwise read the flag its successor had
|
|
80
|
+
// just set, apply the previous key's value and shut the successor out. The
|
|
81
|
+
// second question is also what makes subscribing first safe: a value the
|
|
82
|
+
// listener delivers while the load is in flight is not overwritten by it.
|
|
83
|
+
let isCancelled = false
|
|
84
|
+
|
|
85
|
+
hasAppliedValue.current = false
|
|
40
86
|
|
|
41
|
-
|
|
87
|
+
const loadPersistedValue = async (): Promise<void> => {
|
|
88
|
+
try {
|
|
89
|
+
const persist = await storage.get(safeStorageKey)
|
|
90
|
+
|
|
91
|
+
if (isCancelled || hasAppliedValue.current) return
|
|
92
|
+
|
|
93
|
+
applyValue(getPersistedValue<T>(key, mountInitialValue.current, persist[safeStorageKey]))
|
|
94
|
+
} catch (err) {
|
|
95
|
+
// An extension backend rejects on a quota error or an invalidated
|
|
96
|
+
// extension context. Nothing awaits this load, so an unclaimed
|
|
97
|
+
// rejection terminates the process on Node 15+ and surfaces as an
|
|
98
|
+
// uncaught error in the browser. The initial value is already in
|
|
99
|
+
// state, so the mount stands on it and the failure is reported rather
|
|
100
|
+
// than swallowed, as the synchronous path reports its own.
|
|
101
|
+
console.error("use-persisted-state: Can't read value from storage", err)
|
|
102
|
+
}
|
|
42
103
|
}
|
|
43
104
|
|
|
44
|
-
|
|
45
|
-
}, [initialValue, key])
|
|
105
|
+
loadPersistedValue()
|
|
46
106
|
|
|
47
|
-
|
|
107
|
+
return () => {
|
|
108
|
+
isCancelled = true
|
|
109
|
+
}
|
|
110
|
+
}, [key, applyValue])
|
|
48
111
|
|
|
49
112
|
return [state, setPersistedState]
|
|
50
113
|
}
|
|
@@ -1,47 +1,73 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import { useCallback, useRef, useState } from 'react'
|
|
2
3
|
|
|
3
|
-
import {Storage} from './@types/storage'
|
|
4
|
-
import {PersistedState, UsePersistedState} from './@types/hook'
|
|
4
|
+
import type { Storage } from './@types/storage'
|
|
5
|
+
import type { PersistedState, UsePersistedState } from './@types/hook'
|
|
5
6
|
|
|
6
7
|
import useStorageHandler from './utils/use-storage-handler'
|
|
7
8
|
import getNewValue from './utils/get-new-value'
|
|
8
9
|
import getNewItem from './utils/get-new-item'
|
|
9
10
|
import getPersistedValue from './utils/get-persisted-value'
|
|
10
11
|
|
|
11
|
-
export default function createPersistedState(
|
|
12
|
-
storageKey: string,
|
|
13
|
-
storage: Storage,
|
|
14
|
-
): [PersistedState, () => void] {
|
|
12
|
+
export default function createPersistedState(storageKey: string, storage: Storage): [PersistedState, () => void] {
|
|
15
13
|
const safeStorageKey = `persisted_state_hook:${storageKey}`
|
|
16
14
|
const clear = (): void => {
|
|
17
15
|
storage.remove(safeStorageKey)
|
|
18
16
|
}
|
|
19
17
|
|
|
18
|
+
const readPersisted = <T>(key: string, initialValue: T): T =>
|
|
19
|
+
getPersistedValue<T>(key, initialValue, storage.get(safeStorageKey)[safeStorageKey])
|
|
20
|
+
|
|
20
21
|
const usePersistedState = <T>(key: string, initialValue: T): UsePersistedState<T> => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// Reading through the initializer keeps the storage out of the render path:
|
|
23
|
+
// the hook runs on every render of every consuming component, so a re-read is
|
|
24
|
+
// only warranted when the key it is asked for actually changes.
|
|
25
|
+
const [state, setState] = useState<T>(() => readPersisted(key, initialValue))
|
|
26
|
+
|
|
27
|
+
// The setter must resolve updater functions against the last value applied,
|
|
28
|
+
// not against the one captured by the render that created it. Reading the
|
|
29
|
+
// render closure collapses updates batched into a single event.
|
|
30
|
+
const latestValue = useRef(state)
|
|
24
31
|
|
|
25
|
-
const
|
|
32
|
+
const applyValue = useCallback((value: T): void => {
|
|
33
|
+
latestValue.current = value
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
setState(value)
|
|
36
|
+
}, [])
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
// A mounted hook handed a different key has to show that key's value before
|
|
39
|
+
// anything can be written, or the next update stores the previous key's value
|
|
40
|
+
// under the new key and destroys what was there. Adjusting during render ties
|
|
41
|
+
// the re-read to the change itself, where an effect would cost a read and a
|
|
42
|
+
// second render on every mount as well.
|
|
43
|
+
const renderedKey = useRef(key)
|
|
31
44
|
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
if (renderedKey.current !== key) {
|
|
46
|
+
renderedKey.current = key
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
applyValue(readPersisted(key, initialValue))
|
|
36
49
|
}
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
// The exact entry this hook last wrote and has not yet seen reported back.
|
|
52
|
+
const pendingOwnWrite = useRef<string | null>(null)
|
|
53
|
+
|
|
54
|
+
const setPersistedState = useCallback(
|
|
55
|
+
(newState: React.SetStateAction<T>): void => {
|
|
56
|
+
const newValue = getNewValue<T>(newState, latestValue.current)
|
|
57
|
+
|
|
58
|
+
applyValue(newValue)
|
|
59
|
+
|
|
60
|
+
const persistedItem = storage.get(safeStorageKey)[safeStorageKey]
|
|
61
|
+
const newItem = getNewItem<T>(key, persistedItem, newValue)
|
|
62
|
+
|
|
63
|
+
pendingOwnWrite.current = newItem
|
|
40
64
|
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
storage.set({ [safeStorageKey]: newItem })
|
|
66
|
+
},
|
|
67
|
+
[key, applyValue],
|
|
68
|
+
)
|
|
43
69
|
|
|
44
|
-
useStorageHandler<T>(key, safeStorageKey,
|
|
70
|
+
useStorageHandler<T>(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrite)
|
|
45
71
|
|
|
46
72
|
return [state, setPersistedState]
|
|
47
73
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import createSyncPersistedState from './create-persisted-state'
|
|
2
2
|
import createAsyncPersistedState from './create-async-persisted-state'
|
|
3
3
|
|
|
4
4
|
import isAsyncStorage from './utils/is-async-storage'
|
|
5
5
|
|
|
6
|
-
import { Storage, AsyncStorage } from './@types/storage'
|
|
7
|
-
import { PersistedState } from './@types/hook'
|
|
6
|
+
import type { Storage, AsyncStorage } from './@types/storage'
|
|
7
|
+
import type { PersistedState } from './@types/hook'
|
|
8
8
|
|
|
9
9
|
export default function createPersistedState<S extends Storage | AsyncStorage>(
|
|
10
10
|
name: string,
|
|
@@ -17,7 +17,12 @@ export default function createPersistedState<S extends Storage | AsyncStorage>(
|
|
|
17
17
|
return createSyncPersistedState(name, storage as Storage)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
export { default as createPersistedState } from './create-persisted-state'
|
|
21
|
+
export { default as createAsyncPersistedState } from './create-async-persisted-state'
|
|
22
|
+
|
|
23
|
+
// The contract a backend implements, and the shape the hook hands back. Both appear in the
|
|
24
|
+
// signatures above, so a consumer writing an adapter or typing a wrapper has to be able to name
|
|
25
|
+
// them from the supported entry point rather than from `lib/`, which the exports map keeps open
|
|
26
|
+
// only as a frozen legacy path.
|
|
27
|
+
export type { AsyncStorage, Storage, StorageChange, StorageChangeEvent, StorageChangeListener } from './@types/storage'
|
|
28
|
+
export type { PersistedState, UsePersistedState } from './@types/hook'
|
|
@@ -1,50 +1,22 @@
|
|
|
1
|
-
import { AsyncStorage
|
|
1
|
+
import type { AsyncStorage } from '../@types/storage'
|
|
2
|
+
import { type Area, createListenerRegistry, toStorageChanges, toStoredItems } from '../utils/extension-storage'
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
local: new Set<StorageChangeListener>(),
|
|
5
|
-
sync: new Set<StorageChangeListener>(),
|
|
6
|
-
managed: new Set<StorageChangeListener>(),
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
type Area = keyof typeof listeners
|
|
10
|
-
|
|
11
|
-
function fireStorageEvent(changes: { [key: string]: StorageChange }, area: Area) {
|
|
12
|
-
listeners[area].forEach(listener => {
|
|
13
|
-
listener(changes)
|
|
14
|
-
})
|
|
15
|
-
}
|
|
4
|
+
const registry = createListenerRegistry()
|
|
16
5
|
|
|
17
6
|
browser.storage.onChanged.addListener((changes, area) => {
|
|
18
|
-
|
|
7
|
+
registry.fire(toStorageChanges(changes), area)
|
|
19
8
|
})
|
|
20
9
|
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
addListener(listener) {
|
|
24
|
-
listeners[area].add(listener)
|
|
25
|
-
},
|
|
26
|
-
removeListener(listener) {
|
|
27
|
-
listeners[area].delete(listener)
|
|
28
|
-
},
|
|
29
|
-
hasListener(listener) {
|
|
30
|
-
return listeners[area].has(listener)
|
|
31
|
-
},
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
10
|
+
// browser.storage is promise-based, unlike its callback-based chrome counterpart.
|
|
35
11
|
const createStorage = (storage: browser.storage.StorageArea, area: Area): AsyncStorage => ({
|
|
36
|
-
get: storage.get,
|
|
12
|
+
get: async keys => toStoredItems(await storage.get(keys)),
|
|
37
13
|
set: storage.set,
|
|
38
14
|
remove: storage.remove,
|
|
39
|
-
onChanged: createOnChanged(area),
|
|
15
|
+
onChanged: registry.createOnChanged(area),
|
|
40
16
|
})
|
|
41
17
|
|
|
42
18
|
const local = createStorage(browser.storage.local, 'local')
|
|
43
19
|
const sync = createStorage(browser.storage.sync, 'sync')
|
|
44
20
|
const managed = createStorage(browser.storage.managed, 'managed')
|
|
45
21
|
|
|
46
|
-
export {
|
|
47
|
-
local,
|
|
48
|
-
sync,
|
|
49
|
-
managed,
|
|
50
|
-
}
|
|
22
|
+
export { local, sync, managed }
|
|
@@ -1,58 +1,36 @@
|
|
|
1
|
-
import { AsyncStorage
|
|
1
|
+
import type { AsyncStorage } from '../@types/storage'
|
|
2
|
+
import { type Area, createListenerRegistry, toStorageChanges, toStoredItems } from '../utils/extension-storage'
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
local: new Set<StorageChangeListener>(),
|
|
5
|
-
sync: new Set<StorageChangeListener>(),
|
|
6
|
-
managed: new Set<StorageChangeListener>(),
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
type Area = keyof typeof listeners
|
|
10
|
-
|
|
11
|
-
function fireStorageEvent(changes: { [key: string]: StorageChange }, area: Area) {
|
|
12
|
-
listeners[area].forEach(listener => {
|
|
13
|
-
listener(changes)
|
|
14
|
-
})
|
|
15
|
-
}
|
|
4
|
+
const registry = createListenerRegistry()
|
|
16
5
|
|
|
17
6
|
chrome.storage.onChanged.addListener((changes, area) => {
|
|
18
|
-
|
|
7
|
+
registry.fire(toStorageChanges(changes), area)
|
|
19
8
|
})
|
|
20
9
|
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
addListener(listener) {
|
|
24
|
-
listeners[area].add(listener)
|
|
25
|
-
},
|
|
26
|
-
removeListener(listener) {
|
|
27
|
-
listeners[area].delete(listener)
|
|
28
|
-
},
|
|
29
|
-
hasListener(listener) {
|
|
30
|
-
return listeners[area].has(listener)
|
|
31
|
-
},
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
10
|
+
// chrome.storage is callback-based, unlike its promise-based firefox counterpart.
|
|
35
11
|
const createStorage = (storage: chrome.storage.StorageArea, area: Area): AsyncStorage => ({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
12
|
+
// Declared async, like the firefox adapter, so that asking whether this
|
|
13
|
+
// storage is asynchronous can be answered from the function itself instead of
|
|
14
|
+
// by calling it — a call here costs a round trip to the extension process.
|
|
15
|
+
get: async keys =>
|
|
16
|
+
new Promise(resolve => {
|
|
17
|
+
storage.get(keys, items => {
|
|
18
|
+
resolve(toStoredItems(items))
|
|
19
|
+
})
|
|
20
|
+
}),
|
|
21
|
+
set: items =>
|
|
22
|
+
new Promise(resolve => {
|
|
23
|
+
storage.set(items, resolve)
|
|
24
|
+
}),
|
|
25
|
+
remove: keys =>
|
|
26
|
+
new Promise(resolve => {
|
|
27
|
+
storage.remove(keys, resolve)
|
|
28
|
+
}),
|
|
29
|
+
onChanged: registry.createOnChanged(area),
|
|
48
30
|
})
|
|
49
31
|
|
|
50
32
|
const local = createStorage(chrome.storage.local, 'local')
|
|
51
33
|
const sync = createStorage(chrome.storage.sync, 'sync')
|
|
52
34
|
const managed = createStorage(chrome.storage.managed, 'managed')
|
|
53
35
|
|
|
54
|
-
export {
|
|
55
|
-
local,
|
|
56
|
-
sync,
|
|
57
|
-
managed,
|
|
58
|
-
}
|
|
36
|
+
export { local, sync, managed }
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import createStorage from '../utils/create-web-storage'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// The typeof guard keeps this import from throwing on server runtimes (SSR),
|
|
4
|
+
// where the browser storage globals are not defined.
|
|
5
|
+
export default createStorage(typeof localStorage !== 'undefined' ? localStorage : undefined)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import createStorage from '../utils/create-web-storage'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// The typeof guard keeps this import from throwing on server runtimes (SSR),
|
|
4
|
+
// where the browser storage globals are not defined.
|
|
5
|
+
export default createStorage(typeof sessionStorage !== 'undefined' ? sessionStorage : undefined)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { StorageChange, StorageChangeEvent, StorageChangeListener } from '../@types/storage'
|
|
2
|
+
|
|
3
|
+
export interface ChangeNotifier {
|
|
4
|
+
fire(changes: { [key: string]: StorageChange }): void
|
|
5
|
+
hasListeners(): boolean
|
|
6
|
+
onChanged: StorageChangeEvent
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Holds the subscribers of a single change source. Each adapter — and, for the
|
|
11
|
+
* extension backends, each storage area — owns one, so a change never reaches
|
|
12
|
+
* listeners that subscribed elsewhere.
|
|
13
|
+
*
|
|
14
|
+
* Routing a backend's events to the right notifier stays with the adapter: the
|
|
15
|
+
* extensions demultiplex one event by area name, the DOM one by storage object.
|
|
16
|
+
*/
|
|
17
|
+
export function createChangeNotifier(): ChangeNotifier {
|
|
18
|
+
const listeners = new Set<StorageChangeListener>()
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
fire(changes) {
|
|
22
|
+
for (const listener of listeners) {
|
|
23
|
+
listener(changes)
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
// Lets an adapter release a shared resource once nobody is listening; the
|
|
27
|
+
// set behind onChanged stays the only count, so the two cannot drift.
|
|
28
|
+
hasListeners() {
|
|
29
|
+
return listeners.size > 0
|
|
30
|
+
},
|
|
31
|
+
onChanged: {
|
|
32
|
+
addListener(listener) {
|
|
33
|
+
listeners.add(listener)
|
|
34
|
+
},
|
|
35
|
+
removeListener(listener) {
|
|
36
|
+
listeners.delete(listener)
|
|
37
|
+
},
|
|
38
|
+
hasListener(listener) {
|
|
39
|
+
return listeners.has(listener)
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|