base.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import type { BaseStorage, StorageConfig, ValueOrUpdate } from './types';
  2. import { SessionAccessLevelEnum, StorageEnum } from './enums';
  3. /**
  4. * Chrome reference error while running `processTailwindFeatures` in tailwindcss.
  5. * To avoid this, we need to check if the globalThis.chrome is available and add fallback logic.
  6. */
  7. const chrome = globalThis.chrome;
  8. /**
  9. * Sets or updates an arbitrary cache with a new value or the result of an update function.
  10. */
  11. async function updateCache<D>(valueOrUpdate: ValueOrUpdate<D>, cache: D | null): Promise<D> {
  12. // Type guard to check if our value or update is a function
  13. function isFunction<D>(value: ValueOrUpdate<D>): value is (prev: D) => D | Promise<D> {
  14. return typeof value === 'function';
  15. }
  16. // Type guard to check in case of a function, if its a Promise
  17. function returnsPromise<D>(func: (prev: D) => D | Promise<D>): func is (prev: D) => Promise<D> {
  18. // Use ReturnType to infer the return type of the function and check if it's a Promise
  19. return (func as (prev: D) => Promise<D>) instanceof Promise;
  20. }
  21. if (isFunction(valueOrUpdate)) {
  22. // Check if the function returns a Promise
  23. if (returnsPromise(valueOrUpdate)) {
  24. return valueOrUpdate(cache as D);
  25. } else {
  26. return valueOrUpdate(cache as D);
  27. }
  28. } else {
  29. return valueOrUpdate;
  30. }
  31. }
  32. /**
  33. * If one session storage needs access from content scripts, we need to enable it globally.
  34. * @default false
  35. */
  36. let globalSessionAccessLevelFlag: StorageConfig['sessionAccessForContentScripts'] = false;
  37. /**
  38. * Checks if the storage permission is granted in the manifest.json.
  39. */
  40. function checkStoragePermission(storageEnum: StorageEnum): void {
  41. if (!chrome) {
  42. return;
  43. }
  44. if (chrome.storage[storageEnum] === undefined) {
  45. throw new Error(`Check your storage permission in manifest.json: ${storageEnum} is not defined`);
  46. }
  47. }
  48. /**
  49. * Creates a storage area for persisting and exchanging data.
  50. */
  51. export function createStorage<D = string>(key: string, fallback: D, config?: StorageConfig<D>): BaseStorage<D> {
  52. let cache: D | null = null;
  53. let initedCache = false;
  54. let listeners: Array<() => void> = [];
  55. const storageEnum = config?.storageEnum ?? StorageEnum.Local;
  56. const liveUpdate = config?.liveUpdate ?? false;
  57. const serialize = config?.serialization?.serialize ?? ((v: D) => v);
  58. const deserialize = config?.serialization?.deserialize ?? (v => v as D);
  59. // Set global session storage access level for StoryType.Session, only when not already done but needed.
  60. if (
  61. globalSessionAccessLevelFlag === false &&
  62. storageEnum === StorageEnum.Session &&
  63. config?.sessionAccessForContentScripts === true
  64. ) {
  65. checkStoragePermission(storageEnum);
  66. chrome?.storage[storageEnum]
  67. .setAccessLevel({
  68. accessLevel: SessionAccessLevelEnum.ExtensionPagesAndContentScripts,
  69. })
  70. .catch(error => {
  71. console.warn(error);
  72. console.warn('Please call setAccessLevel into different context, like a background script.');
  73. });
  74. globalSessionAccessLevelFlag = true;
  75. }
  76. // Register life cycle methods
  77. const get = async (): Promise<D> => {
  78. checkStoragePermission(storageEnum);
  79. const value = await chrome?.storage[storageEnum].get([key]);
  80. if (!value) {
  81. return fallback;
  82. }
  83. return deserialize(value[key]) ?? fallback;
  84. };
  85. const _emitChange = () => {
  86. listeners.forEach(listener => listener());
  87. };
  88. const set = async (valueOrUpdate: ValueOrUpdate<D>) => {
  89. if (!initedCache) {
  90. cache = await get();
  91. }
  92. cache = await updateCache(valueOrUpdate, cache);
  93. await chrome?.storage[storageEnum].set({ [key]: serialize(cache) });
  94. _emitChange();
  95. };
  96. const subscribe = (listener: () => void) => {
  97. listeners = [...listeners, listener];
  98. return () => {
  99. listeners = listeners.filter(l => l !== listener);
  100. };
  101. };
  102. const getSnapshot = () => {
  103. return cache;
  104. };
  105. get().then(data => {
  106. cache = data;
  107. initedCache = true;
  108. _emitChange();
  109. });
  110. // Listener for live updates from the browser
  111. async function _updateFromStorageOnChanged(changes: { [key: string]: chrome.storage.StorageChange }) {
  112. // Check if the key we are listening for is in the changes object
  113. if (changes[key] === undefined) return;
  114. const valueOrUpdate: ValueOrUpdate<D> = deserialize(changes[key].newValue);
  115. if (cache === valueOrUpdate) return;
  116. cache = await updateCache(valueOrUpdate, cache);
  117. _emitChange();
  118. }
  119. // Register listener for live updates for our storage area
  120. if (liveUpdate) {
  121. chrome?.storage[storageEnum].onChanged.addListener(_updateFromStorageOnChanged);
  122. }
  123. return {
  124. get,
  125. set,
  126. getSnapshot,
  127. subscribe,
  128. };
  129. }