1
0

DataStoreSerializer.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. /**
  2. * @module lib/DataStoreSerializer
  3. * This module contains the DataStoreSerializer class, which allows you to import and export serialized DataStore data - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#datastoreserializer)
  4. */
  5. import { getUnsafeWindow, computeHash, type DataStore } from "./index.js";
  6. export type DataStoreSerializerOptions = {
  7. /** Whether to add a checksum to the exported data */
  8. addChecksum?: boolean;
  9. /** Whether to ensure the integrity of the data when importing it (unless the checksum property doesn't exist) */
  10. ensureIntegrity?: boolean;
  11. };
  12. /** Serialized data of a DataStore instance */
  13. export type SerializedDataStore = {
  14. /** The ID of the DataStore instance */
  15. id: string;
  16. /** The serialized data */
  17. data: string;
  18. /** The format version of the data */
  19. formatVersion: number;
  20. /** Whether the data is encoded */
  21. encoded: boolean;
  22. /** The checksum of the data - key is not present for data without a checksum */
  23. checksum?: string;
  24. };
  25. /** Result of {@linkcode DataStoreSerializer.loadStoresData()} */
  26. export type LoadStoresDataResult = {
  27. /** The ID of the DataStore instance */
  28. id: string;
  29. /** The in-memory data object */
  30. data: object;
  31. }
  32. /**
  33. * Allows for easy serialization and deserialization of multiple DataStore instances.
  34. *
  35. * All methods are at least `protected`, so you can easily extend this class and overwrite them to use a different storage method or to add additional functionality.
  36. * Remember that you can call `super.methodName()` in the subclass to access the original method.
  37. *
  38. * - ⚠️ Needs to run in a secure context (HTTPS) due to the use of the SubtleCrypto API if checksumming is enabled.
  39. */
  40. export class DataStoreSerializer {
  41. protected stores: DataStore[];
  42. protected options: Required<DataStoreSerializerOptions>;
  43. constructor(stores: DataStore[], options: DataStoreSerializerOptions = {}) {
  44. if(!getUnsafeWindow().crypto || !getUnsafeWindow().crypto.subtle)
  45. throw new Error("DataStoreSerializer has to run in a secure context (HTTPS)!");
  46. this.stores = stores;
  47. this.options = {
  48. addChecksum: true,
  49. ensureIntegrity: true,
  50. ...options,
  51. };
  52. }
  53. /** Calculates the checksum of a string */
  54. protected async calcChecksum(input: string): Promise<string> {
  55. return computeHash(input, "SHA-256");
  56. }
  57. /** Serializes a DataStore instance */
  58. protected async serializeStore(storeInst: DataStore): Promise<SerializedDataStore> {
  59. const data = storeInst.encodingEnabled()
  60. ? await storeInst.encodeData(JSON.stringify(storeInst.getData()))
  61. : JSON.stringify(storeInst.getData());
  62. const checksum = this.options.addChecksum
  63. ? await this.calcChecksum(data)
  64. : undefined;
  65. return {
  66. id: storeInst.id,
  67. data,
  68. formatVersion: storeInst.formatVersion,
  69. encoded: storeInst.encodingEnabled(),
  70. checksum,
  71. };
  72. }
  73. /** Serializes the data stores into a string */
  74. public async serialize(): Promise<string> {
  75. const serData: SerializedDataStore[] = [];
  76. for(const store of this.stores)
  77. serData.push(await this.serializeStore(store));
  78. return JSON.stringify(serData);
  79. }
  80. /**
  81. * Deserializes the data exported via {@linkcode serialize()} and imports it into the DataStore instances.
  82. * Also triggers the migration process if the data format has changed.
  83. */
  84. public async deserialize(serializedData: string): Promise<void> {
  85. const deserStores: SerializedDataStore[] = JSON.parse(serializedData);
  86. for(const storeData of deserStores) {
  87. const storeInst = this.stores.find(s => s.id === storeData.id);
  88. if(!storeInst)
  89. throw new Error(`DataStore instance with ID "${storeData.id}" not found! Make sure to provide it in the DataStoreSerializer constructor.`);
  90. if(this.options.ensureIntegrity && typeof storeData.checksum === "string") {
  91. const checksum = await this.calcChecksum(storeData.data);
  92. if(checksum !== storeData.checksum)
  93. throw new Error(`Checksum mismatch for DataStore with ID "${storeData.id}"!\nExpected: ${storeData.checksum}\nHas: ${checksum}`);
  94. }
  95. const decodedData = storeData.encoded && storeInst.encodingEnabled()
  96. ? await storeInst.decodeData(storeData.data)
  97. : storeData.data;
  98. if(storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion)
  99. await storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false);
  100. else
  101. await storeInst.setData(JSON.parse(decodedData));
  102. }
  103. }
  104. /**
  105. * Loads the persistent data of the DataStore instances into the in-memory cache.
  106. * Also triggers the migration process if the data format has changed.
  107. * @returns Returns a PromiseSettledResult array with the results of each DataStore instance in the format `{ id: string, data: object }`
  108. */
  109. public async loadStoresData(): Promise<PromiseSettledResult<LoadStoresDataResult>[]> {
  110. return Promise.allSettled(this.stores.map(
  111. async store => ({
  112. id: store.id,
  113. data: await store.loadData(),
  114. })
  115. ));
  116. }
  117. /** Resets the persistent data of the DataStore instances to their default values. */
  118. public async resetStoresData(): Promise<PromiseSettledResult<void>[]> {
  119. return Promise.allSettled(this.stores.map(store => store.saveDefaultData()));
  120. }
  121. /**
  122. * Deletes the persistent data of the DataStore instances.
  123. * Leaves the in-memory data untouched.
  124. */
  125. public async deleteStoresData(): Promise<PromiseSettledResult<void>[]> {
  126. return Promise.allSettled(this.stores.map(store => store.deleteData()));
  127. }
  128. }