@plq/use-persisted-state 1.3.0 → 1.4.1

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.
Files changed (101) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +179 -50
  3. package/esm/index.d.mts +13 -0
  4. package/esm/index.mjs +10 -0
  5. package/esm/storages/browser-storage.d.mts +1 -0
  6. package/esm/storages/browser-storage.mjs +5 -0
  7. package/esm/storages/chrome-storage.d.mts +1 -0
  8. package/esm/storages/chrome-storage.mjs +5 -0
  9. package/esm/storages/local-storage.d.mts +1 -0
  10. package/esm/storages/local-storage.mjs +3 -0
  11. package/esm/storages/session-storage.d.mts +1 -0
  12. package/esm/storages/session-storage.mjs +3 -0
  13. package/lib/@types/storage.d.ts.map +1 -1
  14. package/lib/create-async-persisted-state.d.ts +2 -2
  15. package/lib/create-async-persisted-state.d.ts.map +1 -1
  16. package/lib/create-async-persisted-state.js +136 -17
  17. package/lib/create-async-persisted-state.js.map +1 -1
  18. package/lib/create-persisted-state.d.ts +2 -2
  19. package/lib/create-persisted-state.d.ts.map +1 -1
  20. package/lib/create-persisted-state.js +76 -15
  21. package/lib/create-persisted-state.js.map +1 -1
  22. package/lib/index.d.ts +6 -5
  23. package/lib/index.d.ts.map +1 -1
  24. package/lib/index.js +4 -2
  25. package/lib/index.js.map +1 -1
  26. package/lib/storages/browser-storage.d.ts +2 -2
  27. package/lib/storages/browser-storage.d.ts.map +1 -1
  28. package/lib/storages/browser-storage.js +6 -26
  29. package/lib/storages/browser-storage.js.map +1 -1
  30. package/lib/storages/chrome-storage.d.ts +2 -2
  31. package/lib/storages/chrome-storage.d.ts.map +1 -1
  32. package/lib/storages/chrome-storage.js +10 -27
  33. package/lib/storages/chrome-storage.js.map +1 -1
  34. package/lib/storages/local-storage.d.ts +1 -1
  35. package/lib/storages/local-storage.d.ts.map +1 -1
  36. package/lib/storages/local-storage.js +3 -1
  37. package/lib/storages/local-storage.js.map +1 -1
  38. package/lib/storages/session-storage.d.ts +1 -1
  39. package/lib/storages/session-storage.d.ts.map +1 -1
  40. package/lib/storages/session-storage.js +3 -1
  41. package/lib/storages/session-storage.js.map +1 -1
  42. package/lib/utils/change-notifier.d.ts +18 -0
  43. package/lib/utils/change-notifier.d.ts.map +1 -0
  44. package/lib/utils/change-notifier.js +38 -0
  45. package/lib/utils/change-notifier.js.map +1 -0
  46. package/lib/utils/create-web-storage.d.ts +10 -2
  47. package/lib/utils/create-web-storage.d.ts.map +1 -1
  48. package/lib/utils/create-web-storage.js +72 -75
  49. package/lib/utils/create-web-storage.js.map +1 -1
  50. package/lib/utils/extension-storage.d.ts +38 -0
  51. package/lib/utils/extension-storage.d.ts.map +1 -0
  52. package/lib/utils/extension-storage.js +66 -0
  53. package/lib/utils/extension-storage.js.map +1 -0
  54. package/lib/utils/get-new-item.d.ts +13 -1
  55. package/lib/utils/get-new-item.d.ts.map +1 -1
  56. package/lib/utils/get-new-item.js +27 -7
  57. package/lib/utils/get-new-item.js.map +1 -1
  58. package/lib/utils/get-new-value.d.ts +1 -1
  59. package/lib/utils/get-new-value.d.ts.map +1 -1
  60. package/lib/utils/get-persisted-value.d.ts +8 -1
  61. package/lib/utils/get-persisted-value.d.ts.map +1 -1
  62. package/lib/utils/get-persisted-value.js +19 -5
  63. package/lib/utils/get-persisted-value.js.map +1 -1
  64. package/lib/utils/is-async-storage.d.ts +7 -0
  65. package/lib/utils/is-async-storage.d.ts.map +1 -1
  66. package/lib/utils/is-async-storage.js +51 -20
  67. package/lib/utils/is-async-storage.js.map +1 -1
  68. package/lib/utils/storage-event-router.d.ts +17 -0
  69. package/lib/utils/storage-event-router.d.ts.map +1 -0
  70. package/lib/utils/storage-event-router.js +54 -0
  71. package/lib/utils/storage-event-router.js.map +1 -0
  72. package/lib/utils/use-storage-handler.d.ts +14 -3
  73. package/lib/utils/use-storage-handler.d.ts.map +1 -1
  74. package/lib/utils/use-storage-handler.js +109 -26
  75. package/lib/utils/use-storage-handler.js.map +1 -1
  76. package/package.json +97 -41
  77. package/src/@types/storage.ts +13 -13
  78. package/src/create-async-persisted-state.ts +125 -20
  79. package/src/create-persisted-state.ts +59 -23
  80. package/src/index.ts +12 -7
  81. package/src/storages/browser-storage.ts +8 -36
  82. package/src/storages/chrome-storage.ts +24 -46
  83. package/src/storages/local-storage.ts +3 -1
  84. package/src/storages/session-storage.ts +3 -1
  85. package/src/utils/change-notifier.ts +43 -0
  86. package/src/utils/create-web-storage.ts +74 -77
  87. package/src/utils/extension-storage.ts +79 -0
  88. package/src/utils/get-new-item.ts +32 -9
  89. package/src/utils/get-new-value.ts +1 -1
  90. package/src/utils/get-persisted-value.ts +22 -7
  91. package/src/utils/is-async-storage.ts +72 -27
  92. package/src/utils/storage-event-router.ts +67 -0
  93. package/src/utils/use-storage-handler.ts +144 -36
  94. package/storages/browser-storage.d.ts +1 -0
  95. package/storages/browser-storage.js +11 -0
  96. package/storages/chrome-storage.d.ts +1 -0
  97. package/storages/chrome-storage.js +11 -0
  98. package/storages/local-storage.d.ts +1 -0
  99. package/storages/local-storage.js +9 -0
  100. package/storages/session-storage.d.ts +1 -0
  101. package/storages/session-storage.js +9 -0
@@ -1,12 +1,13 @@
1
- import React, {useEffect, useState} from 'react'
1
+ import type React from 'react'
2
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
3
 
3
- import useStorageHandler from './utils/use-storage-handler'
4
+ import useStorageHandler, { recordOwnWrite } 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,
@@ -14,37 +15,141 @@ export default function createAsyncPersistedState<S extends AsyncStorage>(
14
15
  ): [PersistedState, () => Promise<void>] {
15
16
  const safeStorageKey = `persisted_state_hook:${storageKey}`
16
17
 
18
+ // Every hook this factory makes lives in one entry, and storing a value means
19
+ // reading that entry, merging one key into it and writing all of it back, with
20
+ // a suspension point on either side of the merge. Left to overlap, a second
21
+ // writer merges into a snapshot taken before the first one landed and stores
22
+ // it: the first writer's key is gone from storage while its value is still on
23
+ // screen, and nothing reports the disagreement. It is the entry, not the hook,
24
+ // that has to be taken one at a time, so the chain belongs to the factory.
25
+ let entryWrites: Promise<unknown> = Promise.resolve()
26
+
17
27
  const clear = (): Promise<void> => {
18
- return storage.remove(safeStorageKey)
28
+ // Removing the entry changes it as much as storing does, so it takes its turn
29
+ // in the same chain. Outside it, a write already queued lands after the
30
+ // removal and brings back what was cleared - and "clear this data" is the one
31
+ // request that cannot be allowed to half happen.
32
+ const removal = entryWrites.then(() => storage.remove(safeStorageKey))
33
+
34
+ entryWrites = removal.catch(() => undefined)
35
+
36
+ return removal
37
+ }
38
+
39
+ const commitEntry = <T>(key: string, newValue: T, pendingOwnWrites: React.RefObject<string[]>): Promise<void> => {
40
+ const write = entryWrites.then(async () => {
41
+ const persistedItem = await storage.get(safeStorageKey)
42
+ let newItem: string
43
+
44
+ try {
45
+ newItem = getNewItem<T>(key, persistedItem[safeStorageKey], newValue)
46
+ } catch {
47
+ // Refused, and reported where it was refused. A write replaces the whole
48
+ // entry, so an entry that cannot be read is one no write can be built on
49
+ // without dropping every other hook's key; skipping leaves the bytes for
50
+ // a repair to reach. The caller keeps what it set, unpersisted.
51
+ return
52
+ }
53
+
54
+ // Recorded before the write, because the backend may report it before the
55
+ // promise settles.
56
+ recordOwnWrite(pendingOwnWrites, newItem)
57
+
58
+ await storage.set({ [safeStorageKey]: newItem })
59
+ })
60
+
61
+ // The chain has to outlive a failed write, or one backend rejection stops
62
+ // every later write on this entry. The rejection itself is not swallowed: it
63
+ // stays on the promise handed back to the caller that asked for the write.
64
+ entryWrites = write.catch(() => undefined)
65
+
66
+ return write
19
67
  }
20
68
 
21
69
  const usePersistedState = <T>(key: string, initialValue: T | (() => T)): UsePersistedState<T> => {
22
70
  const [state, setState] = useState<T>(initialValue)
23
71
 
24
- const setPersistedState = async (newState: React.SetStateAction<T>): Promise<void> => {
25
- const newValue = getNewValue<T>(newState, state)
72
+ // The setter must resolve updater functions against the last value applied,
73
+ // not against the one captured by the render that created it. Reading the
74
+ // render closure collapses updates batched into a single event.
75
+ const latestValue = useRef(state)
26
76
 
27
- setState(newValue)
77
+ // Whether anything has already put a value in place. A load that settles after
78
+ // the caller set one must not revert it, and no signal local to the load can
79
+ // answer this: the write it loses to happens outside the load entirely.
80
+ const hasAppliedValue = useRef(false)
28
81
 
29
- const persistedItem = await storage.get(safeStorageKey)
30
- const newItem = getNewItem<T>(key, persistedItem[safeStorageKey], newValue)
82
+ const applyValue = useCallback((value: T): void => {
83
+ latestValue.current = value
84
+ hasAppliedValue.current = true
85
+
86
+ setState(value)
87
+ }, [])
88
+
89
+ // The entries this hook has written and not yet seen reported back.
90
+ const pendingOwnWrites = useRef<string[]>([])
91
+
92
+ const setPersistedState = useCallback(
93
+ async (newState: React.SetStateAction<T>): Promise<void> => {
94
+ const newValue = getNewValue<T>(newState, latestValue.current)
95
+
96
+ // Applied before the write is even queued: the caller sees its value at
97
+ // once, and only the trip to storage waits its turn.
98
+ applyValue(newValue)
99
+
100
+ await commitEntry<T>(key, newValue, pendingOwnWrites)
101
+ },
102
+ [key, applyValue],
103
+ )
31
104
 
32
- await storage.set({[safeStorageKey]: newItem})
33
- }
105
+ // As in `useState`, the value the load falls back to is the one given on the
106
+ // first render. Later identities of an inline object must not reload, or the
107
+ // effect re-runs on every render and never settles.
108
+ const mountInitialValue = useRef(initialValue)
109
+
110
+ // Subscribed before the load below, and the order is load-bearing: React runs
111
+ // effects in declaration order, so reading first would leave an interval
112
+ // between the read and the subscription in which a write belongs to neither
113
+ // and is lost. Reading last covers everything written before the subscription
114
+ // began, and the listener covers everything after.
115
+ useStorageHandler<T>(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrites)
34
116
 
35
117
  useEffect(() => {
36
- const setInitialValue = async () => {
37
- const persist = await storage.get(safeStorageKey)
38
- const persistedState = persist[safeStorageKey]
39
- const initialOrPersistedValue = getPersistedValue<T>(key, initialValue, persistedState)
118
+ // Two separate questions, and one cell cannot hold both: whether this load
119
+ // is still the current one, which only the closure it belongs to can answer,
120
+ // and whether a value has been applied meanwhile, which outlives it. A load
121
+ // abandoned by a key change would otherwise read the flag its successor had
122
+ // just set, apply the previous key's value and shut the successor out. The
123
+ // second question is also what makes subscribing first safe: a value the
124
+ // listener delivers while the load is in flight is not overwritten by it.
125
+ let isCancelled = false
126
+
127
+ hasAppliedValue.current = false
128
+
129
+ const loadPersistedValue = async (): Promise<void> => {
130
+ try {
131
+ const persist = await storage.get(safeStorageKey)
40
132
 
41
- setState(initialOrPersistedValue)
133
+ if (isCancelled || hasAppliedValue.current) return
134
+
135
+ applyValue(getPersistedValue<T>(key, mountInitialValue.current, persist[safeStorageKey]))
136
+ } catch (err) {
137
+ // An extension backend rejects on a quota error or an invalidated
138
+ // extension context. Nothing awaits this load, so an unclaimed
139
+ // rejection terminates the process on Node 15+ and surfaces as an
140
+ // uncaught error in the browser. The initial value is already in
141
+ // state, so the mount stands on it and the failure is reported rather
142
+ // than swallowed, as the synchronous path reports its own.
143
+ console.error("use-persisted-state: Can't read value from storage", err)
144
+ }
42
145
  }
43
146
 
44
- setInitialValue()
45
- }, [initialValue, key])
147
+ loadPersistedValue()
46
148
 
47
- useStorageHandler<T>(key, safeStorageKey, setState, storage, initialValue)
149
+ return () => {
150
+ isCancelled = true
151
+ }
152
+ }, [key, applyValue])
48
153
 
49
154
  return [state, setPersistedState]
50
155
  }
@@ -1,47 +1,83 @@
1
- import React, {useEffect, useState} from '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
- import useStorageHandler from './utils/use-storage-handler'
7
+ import useStorageHandler, { recordOwnWrite } 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
- const persist = storage.get(safeStorageKey)
22
- const persistedState = persist[safeStorageKey]
23
- const initialOrPersistedValue = getPersistedValue<T>(key, initialValue, persistedState)
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 [state, setState] = useState<T>(initialOrPersistedValue)
32
+ const applyValue = useCallback((value: T): void => {
33
+ latestValue.current = value
26
34
 
27
- const setPersistedState = (newState: React.SetStateAction<T>): void => {
28
- const newValue = getNewValue<T>(newState, state)
35
+ setState(value)
36
+ }, [])
29
37
 
30
- setState(newValue)
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
- const persistedItem = storage.get(safeStorageKey)[safeStorageKey]
33
- const newItem = getNewItem<T>(key, persistedItem, newValue)
45
+ if (renderedKey.current !== key) {
46
+ renderedKey.current = key
34
47
 
35
- storage.set({[safeStorageKey]: newItem})
48
+ applyValue(readPersisted(key, initialValue))
36
49
  }
37
50
 
38
- useEffect(() => {
39
- const initialOrPersistedValue = getPersistedValue<T>(key, initialValue, persistedState)
51
+ // The entries this hook has written and not yet seen reported back.
52
+ const pendingOwnWrites = useRef<string[]>([])
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
+ let newItem: string
62
+
63
+ try {
64
+ newItem = getNewItem<T>(key, persistedItem, newValue)
65
+ } catch {
66
+ // Refused, and reported where it was refused. A write replaces the whole
67
+ // entry, so an entry that cannot be read is one no write can be built on
68
+ // without dropping every other hook's key; skipping leaves the bytes for
69
+ // a repair to reach. The caller keeps what it set, unpersisted.
70
+ return
71
+ }
72
+
73
+ recordOwnWrite(pendingOwnWrites, newItem)
40
74
 
41
- setState(initialOrPersistedValue)
42
- }, [initialValue, key, persistedState])
75
+ storage.set({ [safeStorageKey]: newItem })
76
+ },
77
+ [key, applyValue],
78
+ )
43
79
 
44
- useStorageHandler<T>(key, safeStorageKey, setState, storage, initialValue)
80
+ useStorageHandler<T>(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrites)
45
81
 
46
82
  return [state, setPersistedState]
47
83
  }
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { default as createSyncPersistedState } from './create-persisted-state'
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
- createSyncPersistedState as createPersistedState,
22
- createAsyncPersistedState,
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, StorageChange, StorageChangeEvent, StorageChangeListener } from '../@types/storage'
1
+ import type { AsyncStorage } from '../@types/storage'
2
+ import { type Area, createListenerRegistry, toStorageChanges, toStoredItems } from '../utils/extension-storage'
2
3
 
3
- const listeners = {
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
- fireStorageEvent(changes, area as Area)
7
+ registry.fire(toStorageChanges(changes), area)
19
8
  })
20
9
 
21
- function createOnChanged(area: Area): StorageChangeEvent {
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, StorageChange, StorageChangeEvent, StorageChangeListener } from '../@types/storage'
1
+ import type { AsyncStorage } from '../@types/storage'
2
+ import { type Area, createListenerRegistry, toStorageChanges, toStoredItems } from '../utils/extension-storage'
2
3
 
3
- const listeners = {
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
- fireStorageEvent(changes, area as Area)
7
+ registry.fire(toStorageChanges(changes), area)
19
8
  })
20
9
 
21
- function createOnChanged(area: Area): StorageChangeEvent {
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
- get: keys => new Promise(resolve => {
37
- storage.get(keys, items => {
38
- resolve(items)
39
- })
40
- }),
41
- set: items => new Promise(resolve => {
42
- storage.set(items, resolve)
43
- }),
44
- remove: keys => new Promise(resolve => {
45
- storage.remove(keys, resolve)
46
- }),
47
- onChanged: createOnChanged(area),
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
- export default createStorage(localStorage)
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
- export default createStorage(sessionStorage)
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
+ }