@verdant-web/store 3.12.1 → 4.0.0-next.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.
Files changed (279) hide show
  1. package/dist/bundle/index.js +11 -13
  2. package/dist/bundle/index.js.map +4 -4
  3. package/dist/esm/__tests__/batching.test.js +5 -5
  4. package/dist/esm/__tests__/batching.test.js.map +1 -1
  5. package/dist/esm/__tests__/entities.test.js +1 -1
  6. package/dist/esm/__tests__/entities.test.js.map +1 -1
  7. package/dist/esm/__tests__/fixtures/testStorage.d.ts +1 -3
  8. package/dist/esm/__tests__/fixtures/testStorage.js +3 -3
  9. package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
  10. package/dist/esm/__tests__/queries.test.js.map +1 -1
  11. package/dist/esm/backup.d.ts +3 -4
  12. package/dist/esm/backup.js.map +1 -1
  13. package/dist/esm/client/Client.d.ts +28 -33
  14. package/dist/esm/client/Client.js +50 -161
  15. package/dist/esm/client/Client.js.map +1 -1
  16. package/dist/esm/client/ClientDescriptor.d.ts +8 -11
  17. package/dist/esm/client/ClientDescriptor.js +39 -141
  18. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  19. package/dist/esm/context/Time.d.ts +13 -0
  20. package/dist/esm/context/Time.js +27 -0
  21. package/dist/esm/context/Time.js.map +1 -0
  22. package/dist/esm/context/context.d.ts +170 -0
  23. package/dist/esm/{context.js.map → context/context.js.map} +1 -1
  24. package/dist/esm/entities/DocumentManager.js.map +1 -1
  25. package/dist/esm/entities/Entity.d.ts +4 -5
  26. package/dist/esm/entities/Entity.js +5 -3
  27. package/dist/esm/entities/Entity.js.map +1 -1
  28. package/dist/esm/entities/Entity.test.js +4 -3
  29. package/dist/esm/entities/Entity.test.js.map +1 -1
  30. package/dist/esm/entities/EntityCache.d.ts +0 -3
  31. package/dist/esm/entities/EntityCache.js +0 -9
  32. package/dist/esm/entities/EntityCache.js.map +1 -1
  33. package/dist/esm/entities/EntityMetadata.d.ts +1 -1
  34. package/dist/esm/entities/EntityMetadata.js +6 -5
  35. package/dist/esm/entities/EntityMetadata.js.map +1 -1
  36. package/dist/esm/entities/EntityStore.d.ts +2 -6
  37. package/dist/esm/entities/EntityStore.js +22 -16
  38. package/dist/esm/entities/EntityStore.js.map +1 -1
  39. package/dist/esm/entities/OperationBatcher.d.ts +2 -5
  40. package/dist/esm/entities/OperationBatcher.js +9 -7
  41. package/dist/esm/entities/OperationBatcher.js.map +1 -1
  42. package/dist/esm/errors.d.ts +8 -0
  43. package/dist/esm/errors.js +12 -0
  44. package/dist/esm/errors.js.map +1 -0
  45. package/dist/esm/files/EntityFile.d.ts +6 -3
  46. package/dist/esm/files/EntityFile.js +22 -19
  47. package/dist/esm/files/EntityFile.js.map +1 -1
  48. package/dist/esm/files/FileManager.d.ts +8 -39
  49. package/dist/esm/files/FileManager.js +15 -170
  50. package/dist/esm/files/FileManager.js.map +1 -1
  51. package/dist/esm/files/utils.d.ts +0 -1
  52. package/dist/esm/files/utils.js +0 -14
  53. package/dist/esm/files/utils.js.map +1 -1
  54. package/dist/esm/index.d.ts +1 -2
  55. package/dist/esm/index.js +0 -1
  56. package/dist/esm/index.js.map +1 -1
  57. package/dist/esm/{metadata → persistence}/MessageCreator.d.ts +5 -6
  58. package/dist/esm/{metadata → persistence}/MessageCreator.js +31 -38
  59. package/dist/esm/persistence/MessageCreator.js.map +1 -0
  60. package/dist/esm/persistence/PersistenceFiles.d.ts +48 -0
  61. package/dist/esm/persistence/PersistenceFiles.js +160 -0
  62. package/dist/esm/persistence/PersistenceFiles.js.map +1 -0
  63. package/dist/esm/persistence/PersistenceMetadata.d.ts +69 -0
  64. package/dist/esm/persistence/PersistenceMetadata.js +302 -0
  65. package/dist/esm/persistence/PersistenceMetadata.js.map +1 -0
  66. package/dist/esm/persistence/PersistenceQueries.d.ts +34 -0
  67. package/dist/esm/persistence/PersistenceQueries.js +15 -0
  68. package/dist/esm/persistence/PersistenceQueries.js.map +1 -0
  69. package/dist/esm/persistence/PersistenceRebaser.d.ts +32 -0
  70. package/dist/esm/persistence/PersistenceRebaser.js +120 -0
  71. package/dist/esm/persistence/PersistenceRebaser.js.map +1 -0
  72. package/dist/esm/{IDBService.d.ts → persistence/idb/IdbService.d.ts} +9 -7
  73. package/dist/esm/{IDBService.js → persistence/idb/IdbService.js} +29 -8
  74. package/dist/esm/persistence/idb/IdbService.js.map +1 -0
  75. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.d.ts +58 -0
  76. package/dist/esm/{files/FileStorage.js → persistence/idb/files/IdbPersistenceFileDb.js} +85 -50
  77. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js.map +1 -0
  78. package/dist/esm/persistence/idb/idbPersistence.d.ts +19 -0
  79. package/dist/esm/persistence/idb/idbPersistence.js +80 -0
  80. package/dist/esm/persistence/idb/idbPersistence.js.map +1 -0
  81. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.d.ts +72 -0
  82. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js +235 -0
  83. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js.map +1 -0
  84. package/dist/esm/{metadata → persistence/idb/metadata}/openMetadataDatabase.d.ts +3 -1
  85. package/dist/esm/{metadata → persistence/idb/metadata}/openMetadataDatabase.js +12 -3
  86. package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js.map +1 -0
  87. package/dist/esm/persistence/idb/queries/IdbQueryDb.d.ts +41 -0
  88. package/dist/esm/persistence/idb/queries/IdbQueryDb.js +174 -0
  89. package/dist/esm/persistence/idb/queries/IdbQueryDb.js.map +1 -0
  90. package/dist/esm/{migration → persistence/idb/queries/migration}/db.d.ts +1 -1
  91. package/dist/esm/{migration → persistence/idb/queries/migration}/db.js +10 -48
  92. package/dist/esm/persistence/idb/queries/migration/db.js.map +1 -0
  93. package/dist/esm/persistence/idb/queries/migration/engine.d.ts +12 -0
  94. package/dist/esm/{migration → persistence/idb/queries/migration}/engine.js +29 -46
  95. package/dist/esm/persistence/idb/queries/migration/engine.js.map +1 -0
  96. package/dist/esm/{migration → persistence/idb/queries/migration}/migrations.d.ts +1 -3
  97. package/dist/esm/{migration → persistence/idb/queries/migration}/migrations.js +11 -10
  98. package/dist/esm/persistence/idb/queries/migration/migrations.js.map +1 -0
  99. package/dist/esm/{migration → persistence/idb/queries/migration}/openQueryDatabase.d.ts +1 -3
  100. package/dist/esm/{migration → persistence/idb/queries/migration}/openQueryDatabase.js +4 -7
  101. package/dist/esm/persistence/idb/queries/migration/openQueryDatabase.js.map +1 -0
  102. package/dist/esm/{migration → persistence/idb/queries/migration}/paths.js +2 -2
  103. package/dist/esm/persistence/idb/queries/migration/paths.js.map +1 -0
  104. package/dist/esm/persistence/idb/queries/migration/paths.test.js.map +1 -0
  105. package/dist/esm/persistence/idb/queries/migration/types.d.ts +6 -0
  106. package/dist/esm/persistence/idb/queries/migration/types.js.map +1 -0
  107. package/dist/esm/persistence/idb/queries/ranges.d.ts +2 -0
  108. package/dist/esm/persistence/idb/queries/ranges.js +66 -0
  109. package/dist/esm/persistence/idb/queries/ranges.js.map +1 -0
  110. package/dist/esm/{idb.d.ts → persistence/idb/util.d.ts} +11 -0
  111. package/dist/esm/{idb.js → persistence/idb/util.js} +58 -1
  112. package/dist/esm/persistence/idb/util.js.map +1 -0
  113. package/dist/esm/persistence/interfaces.d.ts +181 -0
  114. package/dist/esm/persistence/interfaces.js +2 -0
  115. package/dist/esm/persistence/interfaces.js.map +1 -0
  116. package/dist/esm/persistence/persistence.d.ts +4 -0
  117. package/dist/esm/persistence/persistence.js +126 -0
  118. package/dist/esm/persistence/persistence.js.map +1 -0
  119. package/dist/esm/queries/BaseQuery.d.ts +2 -1
  120. package/dist/esm/queries/BaseQuery.js +3 -0
  121. package/dist/esm/queries/BaseQuery.js.map +1 -1
  122. package/dist/esm/queries/CollectionQueries.d.ts +1 -1
  123. package/dist/esm/queries/FindAllQuery.js +1 -3
  124. package/dist/esm/queries/FindAllQuery.js.map +1 -1
  125. package/dist/esm/queries/FindInfiniteQuery.js +2 -5
  126. package/dist/esm/queries/FindInfiniteQuery.js.map +1 -1
  127. package/dist/esm/queries/FindOneQuery.js +1 -3
  128. package/dist/esm/queries/FindOneQuery.js.map +1 -1
  129. package/dist/esm/queries/FindPageQuery.js +1 -3
  130. package/dist/esm/queries/FindPageQuery.js.map +1 -1
  131. package/dist/esm/queries/QueryCache.d.ts +1 -1
  132. package/dist/esm/queries/QueryCache.js +4 -0
  133. package/dist/esm/queries/QueryCache.js.map +1 -1
  134. package/dist/esm/sync/FileSync.d.ts +23 -8
  135. package/dist/esm/sync/FileSync.js +76 -28
  136. package/dist/esm/sync/FileSync.js.map +1 -1
  137. package/dist/esm/sync/PresenceManager.d.ts +4 -3
  138. package/dist/esm/sync/PresenceManager.js +2 -2
  139. package/dist/esm/sync/PresenceManager.js.map +1 -1
  140. package/dist/esm/sync/PushPullSync.d.ts +4 -6
  141. package/dist/esm/sync/PushPullSync.js +13 -12
  142. package/dist/esm/sync/PushPullSync.js.map +1 -1
  143. package/dist/esm/sync/Sync.d.ts +9 -11
  144. package/dist/esm/sync/Sync.js +34 -29
  145. package/dist/esm/sync/Sync.js.map +1 -1
  146. package/dist/esm/sync/WebSocketSync.d.ts +4 -6
  147. package/dist/esm/sync/WebSocketSync.js +20 -22
  148. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  149. package/dist/esm/utils/Disposable.d.ts +5 -2
  150. package/dist/esm/utils/Disposable.js +3 -2
  151. package/dist/esm/utils/Disposable.js.map +1 -1
  152. package/dist/esm/utils/wip.d.ts +2 -0
  153. package/dist/esm/utils/wip.js +5 -0
  154. package/dist/esm/utils/wip.js.map +1 -0
  155. package/package.json +2 -2
  156. package/src/__tests__/batching.test.ts +6 -6
  157. package/src/__tests__/entities.test.ts +1 -1
  158. package/src/__tests__/fixtures/testStorage.ts +2 -10
  159. package/src/__tests__/queries.test.ts +1 -1
  160. package/src/backup.ts +3 -4
  161. package/src/client/Client.ts +69 -226
  162. package/src/client/ClientDescriptor.ts +53 -184
  163. package/src/context/Time.ts +35 -0
  164. package/src/context/context.ts +200 -0
  165. package/src/entities/DocumentManager.ts +0 -3
  166. package/src/entities/Entity.test.ts +9 -9
  167. package/src/entities/Entity.ts +6 -12
  168. package/src/entities/EntityCache.ts +0 -9
  169. package/src/entities/EntityMetadata.ts +4 -4
  170. package/src/entities/EntityStore.ts +26 -29
  171. package/src/entities/OperationBatcher.ts +9 -11
  172. package/src/errors.ts +13 -0
  173. package/src/files/EntityFile.ts +16 -5
  174. package/src/files/FileManager.ts +18 -245
  175. package/src/files/utils.ts +0 -15
  176. package/src/index.ts +2 -1
  177. package/src/{metadata → persistence}/MessageCreator.ts +46 -36
  178. package/src/persistence/PersistenceFiles.ts +227 -0
  179. package/src/persistence/PersistenceMetadata.ts +425 -0
  180. package/src/persistence/PersistenceQueries.ts +22 -0
  181. package/src/persistence/PersistenceRebaser.ts +171 -0
  182. package/src/{IDBService.ts → persistence/idb/IdbService.ts} +45 -12
  183. package/src/{files/FileStorage.ts → persistence/idb/files/IdbPersistenceFileDb.ts} +128 -86
  184. package/src/persistence/idb/idbPersistence.ts +116 -0
  185. package/src/persistence/idb/metadata/IdbMetadataDb.ts +460 -0
  186. package/src/{metadata → persistence/idb/metadata}/openMetadataDatabase.ts +21 -4
  187. package/src/persistence/idb/queries/IdbQueryDb.ts +251 -0
  188. package/src/{migration → persistence/idb/queries/migration}/db.ts +18 -72
  189. package/src/{migration → persistence/idb/queries/migration}/engine.ts +39 -62
  190. package/src/{migration → persistence/idb/queries/migration}/migrations.ts +13 -18
  191. package/src/{migration → persistence/idb/queries/migration}/openQueryDatabase.ts +5 -14
  192. package/src/{migration → persistence/idb/queries/migration}/paths.ts +4 -3
  193. package/src/persistence/idb/queries/migration/types.ts +8 -0
  194. package/src/persistence/idb/queries/ranges.ts +107 -0
  195. package/src/{idb.ts → persistence/idb/util.ts} +75 -0
  196. package/src/persistence/interfaces.ts +240 -0
  197. package/src/persistence/persistence.ts +223 -0
  198. package/src/queries/BaseQuery.ts +5 -1
  199. package/src/queries/CollectionQueries.ts +2 -2
  200. package/src/queries/FindAllQuery.ts +1 -3
  201. package/src/queries/FindInfiniteQuery.ts +2 -5
  202. package/src/queries/FindOneQuery.ts +1 -3
  203. package/src/queries/FindPageQuery.ts +1 -3
  204. package/src/queries/QueryCache.ts +20 -1
  205. package/src/sync/FileSync.ts +93 -30
  206. package/src/sync/PresenceManager.ts +5 -7
  207. package/src/sync/PushPullSync.ts +23 -19
  208. package/src/sync/Sync.ts +45 -36
  209. package/src/sync/WebSocketSync.ts +41 -27
  210. package/src/utils/Disposable.ts +7 -4
  211. package/src/utils/wip.ts +5 -0
  212. package/dist/esm/IDBService.js.map +0 -1
  213. package/dist/esm/__tests__/legacyOids.test.d.ts +0 -1
  214. package/dist/esm/__tests__/legacyOids.test.js +0 -352
  215. package/dist/esm/__tests__/legacyOids.test.js.map +0 -1
  216. package/dist/esm/context.d.ts +0 -45
  217. package/dist/esm/files/FileStorage.d.ts +0 -47
  218. package/dist/esm/files/FileStorage.js.map +0 -1
  219. package/dist/esm/idb.js.map +0 -1
  220. package/dist/esm/metadata/AckInfoStore.d.ts +0 -10
  221. package/dist/esm/metadata/AckInfoStore.js +0 -22
  222. package/dist/esm/metadata/AckInfoStore.js.map +0 -1
  223. package/dist/esm/metadata/BaselinesStore.d.ts +0 -40
  224. package/dist/esm/metadata/BaselinesStore.js +0 -102
  225. package/dist/esm/metadata/BaselinesStore.js.map +0 -1
  226. package/dist/esm/metadata/LocalReplicaStore.d.ts +0 -19
  227. package/dist/esm/metadata/LocalReplicaStore.js +0 -56
  228. package/dist/esm/metadata/LocalReplicaStore.js.map +0 -1
  229. package/dist/esm/metadata/MessageCreator.js.map +0 -1
  230. package/dist/esm/metadata/Metadata.d.ts +0 -146
  231. package/dist/esm/metadata/Metadata.js +0 -452
  232. package/dist/esm/metadata/Metadata.js.map +0 -1
  233. package/dist/esm/metadata/OperationsStore.d.ts +0 -62
  234. package/dist/esm/metadata/OperationsStore.js +0 -175
  235. package/dist/esm/metadata/OperationsStore.js.map +0 -1
  236. package/dist/esm/metadata/SchemaStore.d.ts +0 -9
  237. package/dist/esm/metadata/SchemaStore.js +0 -35
  238. package/dist/esm/metadata/SchemaStore.js.map +0 -1
  239. package/dist/esm/metadata/openMetadataDatabase.js.map +0 -1
  240. package/dist/esm/migration/db.js.map +0 -1
  241. package/dist/esm/migration/engine.d.ts +0 -15
  242. package/dist/esm/migration/engine.js.map +0 -1
  243. package/dist/esm/migration/errors.d.ts +0 -5
  244. package/dist/esm/migration/errors.js +0 -8
  245. package/dist/esm/migration/errors.js.map +0 -1
  246. package/dist/esm/migration/migrations.js.map +0 -1
  247. package/dist/esm/migration/openQueryDatabase.js.map +0 -1
  248. package/dist/esm/migration/openWIPDatabase.d.ts +0 -11
  249. package/dist/esm/migration/openWIPDatabase.js +0 -65
  250. package/dist/esm/migration/openWIPDatabase.js.map +0 -1
  251. package/dist/esm/migration/paths.js.map +0 -1
  252. package/dist/esm/migration/paths.test.js.map +0 -1
  253. package/dist/esm/migration/types.d.ts +0 -3
  254. package/dist/esm/migration/types.js.map +0 -1
  255. package/dist/esm/queries/QueryableStorage.d.ts +0 -20
  256. package/dist/esm/queries/QueryableStorage.js +0 -90
  257. package/dist/esm/queries/QueryableStorage.js.map +0 -1
  258. package/dist/esm/queries/dbQueries.d.ts +0 -22
  259. package/dist/esm/queries/dbQueries.js +0 -130
  260. package/dist/esm/queries/dbQueries.js.map +0 -1
  261. package/src/__tests__/legacyOids.test.ts +0 -375
  262. package/src/context.ts +0 -55
  263. package/src/metadata/AckInfoStore.ts +0 -30
  264. package/src/metadata/BaselinesStore.ts +0 -188
  265. package/src/metadata/LocalReplicaStore.ts +0 -79
  266. package/src/metadata/Metadata.ts +0 -685
  267. package/src/metadata/OperationsStore.ts +0 -332
  268. package/src/metadata/SchemaStore.ts +0 -47
  269. package/src/migration/errors.ts +0 -7
  270. package/src/migration/openWIPDatabase.ts +0 -97
  271. package/src/migration/types.ts +0 -4
  272. package/src/queries/QueryableStorage.ts +0 -122
  273. package/src/queries/dbQueries.ts +0 -161
  274. /package/dist/esm/{context.js → context/context.js} +0 -0
  275. /package/dist/esm/{migration → persistence/idb/queries/migration}/paths.d.ts +0 -0
  276. /package/dist/esm/{migration → persistence/idb/queries/migration}/paths.test.d.ts +0 -0
  277. /package/dist/esm/{migration → persistence/idb/queries/migration}/paths.test.js +0 -0
  278. /package/dist/esm/{migration → persistence/idb/queries/migration}/types.js +0 -0
  279. /package/src/{migration → persistence/idb/queries/migration}/paths.test.ts +0 -0
@@ -0,0 +1,227 @@
1
+ import { FileData, FileRef } from '@verdant-web/common';
2
+ import { Context, FileConfig } from '../context/context.js';
3
+ import { PersistedFileData, PersistenceFileDb } from './interfaces.js';
4
+ import { Disposable } from '../utils/Disposable.js';
5
+
6
+ export class PersistenceFiles extends Disposable {
7
+ constructor(
8
+ private db: PersistenceFileDb,
9
+ private context: Omit<Context, 'queries'>,
10
+ ) {
11
+ super();
12
+ context.internalEvents.subscribe('filesDeleted', this.onFileRefsDeleted);
13
+ this.compose(this.db);
14
+ // on startup, try deleting old files.
15
+ this.cleanupDeletedFiles();
16
+ }
17
+
18
+ private get config(): Required<FileConfig> {
19
+ return {
20
+ canCleanupDeletedFile(fileData) {
21
+ return (
22
+ fileData.deletedAt !== null &&
23
+ fileData.deletedAt < Date.now() - 1000 * 60 * 24 * 3
24
+ );
25
+ },
26
+ ...this.context.config.files,
27
+ };
28
+ }
29
+
30
+ onServerReset = (since: string | null) =>
31
+ this.db.resetSyncedStatusSince(since);
32
+ add = async (file: FileData, options?: { downloadRemote?: boolean }) => {
33
+ // this method accepts a FileData which refers to a remote
34
+ // file, as well as local files. in the case of a remote file,
35
+ // we actually re-download and upload the file again. this powers
36
+ // the cloning of documents with files; we clone their filedata
37
+ // and re-upload to a new file ID. otherwise, when the cloned
38
+ // filedata was marked deleted, the original file would be deleted
39
+ // and the clone would refer to a missing file.
40
+ if (file.url && !file.file) {
41
+ this.context.log(
42
+ 'debug',
43
+ 'Remote file added to an entity. This usually means an entity was cloned. Downloading remote file...',
44
+ file.id,
45
+ );
46
+ const blob = await this.context.files.downloadRemoteFile(file.url, 0, 3);
47
+ // convert blob to file with name and type
48
+ file.file = new File([blob], file.name, { type: file.type });
49
+ } else if (!file.file) {
50
+ this.context.log(
51
+ 'warn',
52
+ 'File added without a file or URL. This file will not be available for use.',
53
+ file.id,
54
+ );
55
+ }
56
+
57
+ file.remote = false;
58
+ // fire event for processing immediately
59
+ this.context.internalEvents.emit('fileAdded', file);
60
+ // store in persistence db
61
+ await this.db.add(file, options);
62
+ this.context.log(
63
+ 'debug',
64
+ 'File added',
65
+ file.id,
66
+ file.name,
67
+ file.type,
68
+ file.file ? 'with binary file' : file.url ? 'with url' : 'with no data',
69
+ );
70
+ };
71
+ onUploaded = this.db.markUploaded.bind(this.db);
72
+ get = this.db.get.bind(this.db);
73
+ getAll = this.db.getAll.bind(this.db);
74
+ listUnsynced = this.db.listUnsynced.bind(this.db);
75
+ iterateOverPendingDelete = this.db.iterateOverPendingDelete.bind(this.db);
76
+ stats = this.db.stats.bind(this.db);
77
+
78
+ private getFileExportName = (originalFileName: string, id: string) => {
79
+ return `${id}___${originalFileName}`;
80
+ };
81
+ export = async (downloadRemote = false) => {
82
+ const storedFiles = await this.getAll();
83
+ if (downloadRemote) {
84
+ for (const storedFile of storedFiles) {
85
+ // if it doesn't have a buffer, we need to read one from the server
86
+ if (!storedFile.file && storedFile.url) {
87
+ try {
88
+ const blob = await this.downloadRemoteFile(storedFile.url);
89
+ storedFile.file = blob;
90
+ } catch (err) {
91
+ this.context.log(
92
+ 'error',
93
+ "Failed to download file to cache it locally. The file will still be available using its URL. Check the file server's CORS configuration.",
94
+ err,
95
+ );
96
+ }
97
+ }
98
+ }
99
+ }
100
+ // split files into data and files
101
+ const fileData: Array<Omit<PersistedFileData, 'file'>> = [];
102
+ const files: Array<File> = [];
103
+
104
+ for (const fileExport of storedFiles) {
105
+ const file = fileExport.file;
106
+ delete fileExport.file;
107
+ fileData.push(fileExport);
108
+ if (file) {
109
+ // rename with ID
110
+ const asFile = new File(
111
+ [file],
112
+ this.getFileExportName(fileExport.name, fileExport.id),
113
+ {
114
+ type: fileExport.type,
115
+ },
116
+ );
117
+ files.push(asFile);
118
+ } else {
119
+ this.context.log(
120
+ 'warn',
121
+ `File ${fileExport.id} was could not be loaded locally or from the server. It will be missing in the export.`,
122
+ );
123
+ }
124
+ }
125
+ return {
126
+ fileData,
127
+ files,
128
+ };
129
+ };
130
+
131
+ import = async ({
132
+ fileData,
133
+ files,
134
+ }: {
135
+ fileData: Array<Omit<PersistedFileData, 'file'>>;
136
+ files: File[];
137
+ }) => {
138
+ // re-attach files to their file data and import
139
+ const fileToIdMap = new Map(
140
+ files.map((file) => {
141
+ const { id } = this.parseFileExportname(file.name);
142
+ return [id, file];
143
+ }),
144
+ );
145
+ const importedFiles: PersistedFileData[] = fileData.map((fileData) => {
146
+ const file = fileToIdMap.get(fileData.id);
147
+
148
+ if (!file) {
149
+ this.context.log('warn', `File ${fileData.id} was not found in import`);
150
+ return fileData;
151
+ }
152
+
153
+ return {
154
+ ...fileData,
155
+ file,
156
+ };
157
+ });
158
+ await Promise.all(importedFiles.map((file) => this.add(file)));
159
+ };
160
+
161
+ private parseFileExportname = (name: string) => {
162
+ const [id, originalFileName] = name.split('___');
163
+ return { id, originalFileName };
164
+ };
165
+
166
+ downloadRemoteFile = async (url: string, retries = 0, maxRetries = 0) => {
167
+ const resp = await fetch(url, {
168
+ method: 'GET',
169
+ credentials: 'include',
170
+ });
171
+ if (!resp.ok) {
172
+ if (retries < maxRetries) {
173
+ return new Promise<Blob>((resolve, reject) => {
174
+ setTimeout(() => {
175
+ this.downloadRemoteFile(url, retries + 1, maxRetries).then(
176
+ resolve,
177
+ reject,
178
+ );
179
+ }, 1000);
180
+ });
181
+ } else {
182
+ throw new Error(
183
+ `Failed to download file after ${maxRetries} retries (status: ${resp.status})`,
184
+ );
185
+ }
186
+ }
187
+ return await resp.blob();
188
+ };
189
+
190
+ cleanupDeletedFiles = async () => {
191
+ let count = 0;
192
+ let skipCount = 0;
193
+ await this.iterateOverPendingDelete((fileData, store) => {
194
+ if (this.config.canCleanupDeletedFile(fileData)) {
195
+ count++;
196
+ store.delete(fileData.id);
197
+ } else {
198
+ skipCount++;
199
+ }
200
+ });
201
+
202
+ this.context.log(
203
+ 'info',
204
+ `Cleaned up ${count} files, skipped ${skipCount} files`,
205
+ );
206
+ };
207
+
208
+ private onFileRefsDeleted = async (fileRefs: FileRef[]) => {
209
+ const tx = this.db.transaction({
210
+ mode: 'readwrite',
211
+ storeNames: ['files'],
212
+ });
213
+ await Promise.all(
214
+ fileRefs.map(async (fileRef) => {
215
+ try {
216
+ await this.db.markPendingDelete(fileRef.id, { transaction: tx });
217
+ } catch (err) {
218
+ this.context.log('error', 'Failed to mark file for deletion', err);
219
+ }
220
+ }),
221
+ );
222
+ this.context.log(
223
+ 'info',
224
+ `Marked ${fileRefs.length} files as pending delete`,
225
+ );
226
+ };
227
+ }
@@ -0,0 +1,425 @@
1
+ import {
2
+ applyPatch,
3
+ assert,
4
+ assignOid,
5
+ ClientMessage,
6
+ DocumentBaseline,
7
+ EventSubscriber,
8
+ getOidRoot,
9
+ ObjectIdentifier,
10
+ Operation,
11
+ substituteRefsWithObjects,
12
+ } from '@verdant-web/common';
13
+ import {
14
+ AbstractTransaction,
15
+ ClientOperation,
16
+ CommonQueryOptions,
17
+ MetadataExport,
18
+ PersistenceMetadataDb,
19
+ } from './interfaces.js';
20
+ import { InitialContext } from '../context/context.js';
21
+ import { PersistenceRebaser } from './PersistenceRebaser.js';
22
+ import { MessageCreator } from './MessageCreator.js';
23
+ import { Disposable } from '../utils/Disposable.js';
24
+
25
+ export class PersistenceMetadata extends Disposable {
26
+ private rebaser: PersistenceRebaser;
27
+ /** Available to others, like sync... */
28
+ readonly messageCreator: MessageCreator;
29
+ readonly events = new EventSubscriber<{
30
+ syncMessage: (message: ClientMessage) => void;
31
+ }>();
32
+
33
+ constructor(
34
+ private db: PersistenceMetadataDb,
35
+ private ctx: InitialContext,
36
+ ) {
37
+ super();
38
+ this.rebaser = new PersistenceRebaser(db, ctx);
39
+ this.messageCreator = new MessageCreator(db, ctx);
40
+ this.compose(this.db);
41
+ }
42
+
43
+ private insertOperations = async (
44
+ operations: ClientOperation[],
45
+ options?: { transaction?: AbstractTransaction },
46
+ ) => {
47
+ this.ctx.log(
48
+ 'debug',
49
+ `Inserting ${operations.length} operations`,
50
+ operations,
51
+ );
52
+
53
+ const affectedDocumentOids = await this.db.addOperations(
54
+ operations,
55
+ options,
56
+ );
57
+
58
+ for (const op of operations) {
59
+ this.ctx.globalEvents.emit('operation', op);
60
+ }
61
+
62
+ // we can now enqueue and check for rebase opportunities
63
+ if (!this.ctx.config.persistence?.disableRebasing) {
64
+ this.rebaser.tryAutonomousRebase();
65
+ }
66
+
67
+ return affectedDocumentOids;
68
+ };
69
+
70
+ private insertLocalOperations = async (
71
+ operations: Operation[],
72
+ options?: { transaction?: AbstractTransaction },
73
+ ) => {
74
+ if (operations.length === 0) return;
75
+
76
+ // add local flag, in place.
77
+ for (const operation of operations) {
78
+ (operation as ClientOperation).isLocal = true;
79
+ }
80
+ await this.insertOperations(operations as ClientOperation[], options);
81
+
82
+ const message = await this.messageCreator.createOperation({ operations });
83
+ this.events.emit('syncMessage', message);
84
+ };
85
+
86
+ private insertRemoteOperations = async (
87
+ operations: Operation[],
88
+ options?: { transaction?: AbstractTransaction },
89
+ ) => {
90
+ if (operations.length === 0) return [];
91
+
92
+ // add local flag, in place
93
+ for (const operation of operations) {
94
+ (operation as ClientOperation).isLocal = false;
95
+ }
96
+
97
+ await this.insertOperations(operations as ClientOperation[], options);
98
+
99
+ this.ack(operations[operations.length - 1].timestamp);
100
+ };
101
+
102
+ private insertRemoteBaselines = async (
103
+ baselines: DocumentBaseline[],
104
+ options?: { transaction?: AbstractTransaction },
105
+ ) => {
106
+ if (baselines.length === 0) return [];
107
+ this.ctx.log('debug', `Inserting ${baselines.length} remote baselines`);
108
+
109
+ await this.db.setBaselines(baselines, options);
110
+
111
+ // this.ack(baselines[baselines.length - 1].timestamp);
112
+
113
+ const affectedOidSet = new Set<ObjectIdentifier>();
114
+ baselines.forEach((baseline) => {
115
+ affectedOidSet.add(getOidRoot(baseline.oid));
116
+ });
117
+
118
+ return Array.from(affectedOidSet);
119
+ };
120
+
121
+ deleteDocument = async (rootOid: string) => {
122
+ const oids = new Set<ObjectIdentifier>();
123
+ const documentOid = getOidRoot(rootOid);
124
+ assert(documentOid === rootOid, 'Must be root document OID');
125
+ oids.add(documentOid);
126
+ // readwrite mode to block on other write transactions
127
+ const transaction = this.db.transaction({
128
+ storeNames: ['baselines', 'operations'],
129
+ });
130
+ await Promise.all([
131
+ this.db.iterateDocumentBaselines(
132
+ documentOid,
133
+ (baseline) => {
134
+ oids.add(baseline.oid);
135
+ },
136
+ { transaction },
137
+ ),
138
+ this.db.iterateDocumentOperations(
139
+ documentOid,
140
+ (patch) => {
141
+ oids.add(patch.oid);
142
+ },
143
+ { transaction },
144
+ ),
145
+ ]);
146
+ const authz = await this.getDocumentAuthz(documentOid);
147
+ const ops = new Array<Operation>();
148
+ for (const oid of oids) {
149
+ ops.push({
150
+ oid,
151
+ timestamp: this.ctx.time.now,
152
+ data: { op: 'delete' },
153
+ authz,
154
+ });
155
+ }
156
+ return this.insertLocalOperations(ops);
157
+ };
158
+
159
+ deleteCollection = async (collection: string) => {
160
+ const oids = new Set<ObjectIdentifier>();
161
+ const transaction = this.db.transaction({
162
+ storeNames: ['baselines', 'operations'],
163
+ mode: 'readwrite',
164
+ });
165
+ await Promise.all([
166
+ this.db.iterateCollectionBaselines(
167
+ collection,
168
+ (baseline) => {
169
+ oids.add(baseline.oid);
170
+ },
171
+ { transaction },
172
+ ),
173
+ this.db.iterateCollectionOperations(
174
+ collection,
175
+ (patch) => {
176
+ oids.add(patch.oid);
177
+ },
178
+ { transaction },
179
+ ),
180
+ ]);
181
+
182
+ const ops = new Array<Operation>();
183
+ for (const oid of oids) {
184
+ ops.push({
185
+ oid,
186
+ timestamp: this.ctx.time.now,
187
+ data: { op: 'delete' },
188
+ authz: undefined,
189
+ });
190
+ }
191
+
192
+ return this.insertLocalOperations(ops);
193
+ };
194
+
195
+ getDocumentSnapshot = async (
196
+ oid: ObjectIdentifier,
197
+ options: { to?: string } = {},
198
+ ) => {
199
+ const documentOid = getOidRoot(oid);
200
+ assert(documentOid === oid, 'Must be root document OID');
201
+ const transaction = this.db.transaction({
202
+ storeNames: ['baselines', 'operations'],
203
+ mode: 'readwrite',
204
+ });
205
+ const baselines: DocumentBaseline[] = [];
206
+ await this.db.iterateDocumentBaselines(
207
+ documentOid,
208
+ (b) => {
209
+ baselines.push(b);
210
+ },
211
+ {
212
+ transaction,
213
+ },
214
+ );
215
+ const objectMap = new Map<ObjectIdentifier, any>();
216
+ for (const baseline of baselines) {
217
+ if (baseline.snapshot) {
218
+ assignOid(baseline.snapshot, baseline.oid);
219
+ }
220
+ objectMap.set(baseline.oid, baseline.snapshot);
221
+ }
222
+ await this.db.iterateDocumentOperations(
223
+ documentOid,
224
+ (op) => {
225
+ const obj = objectMap.get(op.oid) || undefined;
226
+ const newObj = applyPatch(obj, op.data);
227
+ if (newObj) {
228
+ assignOid(newObj, op.oid);
229
+ }
230
+ objectMap.set(op.oid, newObj);
231
+ },
232
+ {
233
+ transaction,
234
+ // only apply operations up to the current time
235
+ to: options.to || this.ctx.time.now,
236
+ },
237
+ );
238
+ const root = objectMap.get(documentOid);
239
+ if (root) {
240
+ substituteRefsWithObjects(root, objectMap);
241
+ }
242
+ return root;
243
+ };
244
+
245
+ getDocumentData = async (
246
+ oid: ObjectIdentifier,
247
+ options?: { abort?: AbortSignal },
248
+ ) => {
249
+ const transaction = this.db.transaction({
250
+ storeNames: ['baselines', 'operations'],
251
+ abort: options?.abort,
252
+ });
253
+ const baselines: DocumentBaseline[] = [];
254
+ const operations: Record<ObjectIdentifier, Operation[]> = {};
255
+ await Promise.all([
256
+ this.db.iterateDocumentBaselines(
257
+ oid,
258
+ (baseline) => {
259
+ baselines.push(baseline);
260
+ },
261
+ {
262
+ transaction,
263
+ },
264
+ ),
265
+ this.db.iterateDocumentOperations(
266
+ oid,
267
+ (op) => {
268
+ operations[op.oid] ??= [];
269
+ operations[op.oid].push(op);
270
+ },
271
+ { transaction },
272
+ ),
273
+ ]);
274
+ return {
275
+ baselines,
276
+ operations,
277
+ };
278
+ };
279
+
280
+ getDocumentAuthz = async (oid: ObjectIdentifier) => {
281
+ let authz;
282
+ await this.db.iterateEntityOperations(oid, (op) => {
283
+ if (op.data.op === 'initialize') {
284
+ authz = op.authz;
285
+ return true;
286
+ }
287
+ });
288
+ return authz;
289
+ };
290
+
291
+ insertData = async (
292
+ data: {
293
+ baselines?: DocumentBaseline[];
294
+ operations?: Operation[];
295
+ isLocal?: boolean;
296
+ },
297
+ options?: { abort?: AbortSignal },
298
+ ) => {
299
+ const transaction = this.db.transaction({
300
+ storeNames: ['baselines', 'operations'],
301
+ abort: options?.abort,
302
+ mode: 'readwrite',
303
+ });
304
+ if (data.baselines) {
305
+ await this.insertRemoteBaselines(data.baselines, { transaction });
306
+ }
307
+ if (options?.abort?.aborted) return;
308
+ if (data.operations) {
309
+ if (data.isLocal) {
310
+ await this.insertLocalOperations(data.operations, { transaction });
311
+ } else {
312
+ await this.insertRemoteOperations(data.operations, { transaction });
313
+ }
314
+ }
315
+ };
316
+
317
+ updateLastSynced = async (timestamp: string) => {
318
+ if (this.ctx.closing) return;
319
+
320
+ return this.db.updateLocalReplica({
321
+ lastSyncedLogicalTime: timestamp,
322
+ });
323
+ };
324
+ setGlobalAck = async (ack: string) => {
325
+ if (this.ctx.closing) return;
326
+ await this.db.setGlobalAck(ack);
327
+ if (!this.ctx.config.persistence?.disableRebasing) {
328
+ await this.rebaser.scheduleRebase(ack);
329
+ }
330
+ };
331
+
332
+ getLocalReplica = async (options?: CommonQueryOptions) => {
333
+ return this.db.getLocalReplica(options);
334
+ };
335
+
336
+ // used to construct sync messages
337
+ iterateLocalOperations = this.db.iterateLocalOperations;
338
+ iterateAllOperations = this.db.iterateAllOperations;
339
+ iterateAllBaselines = this.db.iterateAllBaselines;
340
+
341
+ reset = this.db.reset;
342
+ stats = this.db.stats;
343
+
344
+ export = async (): Promise<MetadataExport> => {
345
+ const db = this.db;
346
+ const baselines = new Array<DocumentBaseline>();
347
+ const operations = new Array<ClientOperation>();
348
+ const transaction = db.transaction({
349
+ storeNames: ['baselines', 'operations'],
350
+ mode: 'readwrite',
351
+ });
352
+ await this.iterateAllOperations(
353
+ (op) => {
354
+ operations.push(op);
355
+ },
356
+ { transaction },
357
+ );
358
+ await this.iterateAllBaselines(
359
+ (baseline) => {
360
+ baselines.push(baseline);
361
+ },
362
+ { transaction },
363
+ );
364
+ const localReplica = await this.db.getLocalReplica();
365
+ return {
366
+ operations,
367
+ baselines,
368
+ localReplica,
369
+ schemaVersion: this.ctx.schema.version,
370
+ };
371
+ };
372
+
373
+ resetFrom = async (data: MetadataExport) => {
374
+ const db = this.db;
375
+ const transaction = db.transaction({
376
+ storeNames: ['baselines', 'operations', 'info'],
377
+ mode: 'readwrite',
378
+ });
379
+ await this.db.reset({ clearReplica: true, transaction });
380
+ if (data.localReplica) {
381
+ await this.db.updateLocalReplica(
382
+ {
383
+ ackedLogicalTime: data.localReplica.ackedLogicalTime,
384
+ lastSyncedLogicalTime: data.localReplica.lastSyncedLogicalTime,
385
+ },
386
+ { transaction },
387
+ );
388
+ }
389
+ this.ctx.log('debug', 'Resetting metadata from export', data);
390
+ await this.insertData({
391
+ operations: data.operations,
392
+ baselines: data.baselines,
393
+ isLocal: true,
394
+ });
395
+ };
396
+
397
+ manualRebase = async () => {
398
+ if (this.ctx.closing || this.ctx.config.persistence?.disableRebasing)
399
+ return;
400
+ const ackInfo = await this.db.getAckInfo();
401
+ if (ackInfo.globalAckTimestamp) {
402
+ await this.rebaser.scheduleRebase(ackInfo.globalAckTimestamp);
403
+ }
404
+ };
405
+
406
+ private ack = async (timestamp: string) => {
407
+ const localReplicaInfo = await this.db.getLocalReplica();
408
+ // can't ack timestamps from the future.
409
+ if (timestamp > this.ctx.time.now) return;
410
+
411
+ this.events.emit('syncMessage', {
412
+ type: 'ack',
413
+ replicaId: localReplicaInfo.id,
414
+ timestamp,
415
+ });
416
+
417
+ if (
418
+ !this.ctx.closing &&
419
+ (!localReplicaInfo.ackedLogicalTime ||
420
+ timestamp > localReplicaInfo.ackedLogicalTime)
421
+ ) {
422
+ this.db.updateLocalReplica({ ackedLogicalTime: timestamp });
423
+ }
424
+ };
425
+ }
@@ -0,0 +1,22 @@
1
+ import { PersistenceQueryDb } from './interfaces.js';
2
+ import { Context } from '../context/context.js';
3
+ import { Disposable } from '../utils/Disposable.js';
4
+
5
+ export class PersistenceQueries extends Disposable {
6
+ constructor(
7
+ private db: PersistenceQueryDb,
8
+ private ctx: Omit<Context, 'queries'>,
9
+ ) {
10
+ super();
11
+ this.compose(this.db);
12
+ }
13
+
14
+ reset = this.db.reset.bind(this.db);
15
+
16
+ saveEntities = this.db.saveEntities;
17
+
18
+ findOneOid = this.db.findOneOid.bind(this.db);
19
+ findAllOids = this.db.findAllOids.bind(this.db);
20
+
21
+ stats = this.db.stats.bind(this.db);
22
+ }