@verdant-web/store 3.12.0 → 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 (281) 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/entities/types.d.ts +1 -1
  43. package/dist/esm/errors.d.ts +8 -0
  44. package/dist/esm/errors.js +12 -0
  45. package/dist/esm/errors.js.map +1 -0
  46. package/dist/esm/files/EntityFile.d.ts +6 -3
  47. package/dist/esm/files/EntityFile.js +22 -19
  48. package/dist/esm/files/EntityFile.js.map +1 -1
  49. package/dist/esm/files/FileManager.d.ts +8 -39
  50. package/dist/esm/files/FileManager.js +15 -170
  51. package/dist/esm/files/FileManager.js.map +1 -1
  52. package/dist/esm/files/utils.d.ts +0 -1
  53. package/dist/esm/files/utils.js +0 -14
  54. package/dist/esm/files/utils.js.map +1 -1
  55. package/dist/esm/index.d.ts +1 -2
  56. package/dist/esm/index.js +0 -1
  57. package/dist/esm/index.js.map +1 -1
  58. package/dist/esm/{metadata → persistence}/MessageCreator.d.ts +5 -6
  59. package/dist/esm/{metadata → persistence}/MessageCreator.js +31 -38
  60. package/dist/esm/persistence/MessageCreator.js.map +1 -0
  61. package/dist/esm/persistence/PersistenceFiles.d.ts +48 -0
  62. package/dist/esm/persistence/PersistenceFiles.js +160 -0
  63. package/dist/esm/persistence/PersistenceFiles.js.map +1 -0
  64. package/dist/esm/persistence/PersistenceMetadata.d.ts +69 -0
  65. package/dist/esm/persistence/PersistenceMetadata.js +302 -0
  66. package/dist/esm/persistence/PersistenceMetadata.js.map +1 -0
  67. package/dist/esm/persistence/PersistenceQueries.d.ts +34 -0
  68. package/dist/esm/persistence/PersistenceQueries.js +15 -0
  69. package/dist/esm/persistence/PersistenceQueries.js.map +1 -0
  70. package/dist/esm/persistence/PersistenceRebaser.d.ts +32 -0
  71. package/dist/esm/persistence/PersistenceRebaser.js +120 -0
  72. package/dist/esm/persistence/PersistenceRebaser.js.map +1 -0
  73. package/dist/esm/{IDBService.d.ts → persistence/idb/IdbService.d.ts} +9 -7
  74. package/dist/esm/{IDBService.js → persistence/idb/IdbService.js} +29 -8
  75. package/dist/esm/persistence/idb/IdbService.js.map +1 -0
  76. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.d.ts +58 -0
  77. package/dist/esm/{files/FileStorage.js → persistence/idb/files/IdbPersistenceFileDb.js} +85 -50
  78. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js.map +1 -0
  79. package/dist/esm/persistence/idb/idbPersistence.d.ts +19 -0
  80. package/dist/esm/persistence/idb/idbPersistence.js +80 -0
  81. package/dist/esm/persistence/idb/idbPersistence.js.map +1 -0
  82. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.d.ts +72 -0
  83. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js +235 -0
  84. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js.map +1 -0
  85. package/dist/esm/{metadata → persistence/idb/metadata}/openMetadataDatabase.d.ts +3 -1
  86. package/dist/esm/{metadata → persistence/idb/metadata}/openMetadataDatabase.js +12 -3
  87. package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js.map +1 -0
  88. package/dist/esm/persistence/idb/queries/IdbQueryDb.d.ts +41 -0
  89. package/dist/esm/persistence/idb/queries/IdbQueryDb.js +174 -0
  90. package/dist/esm/persistence/idb/queries/IdbQueryDb.js.map +1 -0
  91. package/dist/esm/{migration → persistence/idb/queries/migration}/db.d.ts +1 -1
  92. package/dist/esm/{migration → persistence/idb/queries/migration}/db.js +10 -48
  93. package/dist/esm/persistence/idb/queries/migration/db.js.map +1 -0
  94. package/dist/esm/persistence/idb/queries/migration/engine.d.ts +12 -0
  95. package/dist/esm/{migration → persistence/idb/queries/migration}/engine.js +29 -46
  96. package/dist/esm/persistence/idb/queries/migration/engine.js.map +1 -0
  97. package/dist/esm/{migration → persistence/idb/queries/migration}/migrations.d.ts +1 -3
  98. package/dist/esm/{migration → persistence/idb/queries/migration}/migrations.js +11 -10
  99. package/dist/esm/persistence/idb/queries/migration/migrations.js.map +1 -0
  100. package/dist/esm/{migration → persistence/idb/queries/migration}/openQueryDatabase.d.ts +1 -3
  101. package/dist/esm/{migration → persistence/idb/queries/migration}/openQueryDatabase.js +4 -7
  102. package/dist/esm/persistence/idb/queries/migration/openQueryDatabase.js.map +1 -0
  103. package/dist/esm/{migration → persistence/idb/queries/migration}/paths.js +2 -2
  104. package/dist/esm/persistence/idb/queries/migration/paths.js.map +1 -0
  105. package/dist/esm/persistence/idb/queries/migration/paths.test.js.map +1 -0
  106. package/dist/esm/persistence/idb/queries/migration/types.d.ts +6 -0
  107. package/dist/esm/persistence/idb/queries/migration/types.js.map +1 -0
  108. package/dist/esm/persistence/idb/queries/ranges.d.ts +2 -0
  109. package/dist/esm/persistence/idb/queries/ranges.js +66 -0
  110. package/dist/esm/persistence/idb/queries/ranges.js.map +1 -0
  111. package/dist/esm/{idb.d.ts → persistence/idb/util.d.ts} +11 -0
  112. package/dist/esm/{idb.js → persistence/idb/util.js} +58 -1
  113. package/dist/esm/persistence/idb/util.js.map +1 -0
  114. package/dist/esm/persistence/interfaces.d.ts +181 -0
  115. package/dist/esm/persistence/interfaces.js +2 -0
  116. package/dist/esm/persistence/interfaces.js.map +1 -0
  117. package/dist/esm/persistence/persistence.d.ts +4 -0
  118. package/dist/esm/persistence/persistence.js +126 -0
  119. package/dist/esm/persistence/persistence.js.map +1 -0
  120. package/dist/esm/queries/BaseQuery.d.ts +2 -1
  121. package/dist/esm/queries/BaseQuery.js +3 -0
  122. package/dist/esm/queries/BaseQuery.js.map +1 -1
  123. package/dist/esm/queries/CollectionQueries.d.ts +1 -1
  124. package/dist/esm/queries/FindAllQuery.js +1 -3
  125. package/dist/esm/queries/FindAllQuery.js.map +1 -1
  126. package/dist/esm/queries/FindInfiniteQuery.js +2 -5
  127. package/dist/esm/queries/FindInfiniteQuery.js.map +1 -1
  128. package/dist/esm/queries/FindOneQuery.js +1 -3
  129. package/dist/esm/queries/FindOneQuery.js.map +1 -1
  130. package/dist/esm/queries/FindPageQuery.js +1 -3
  131. package/dist/esm/queries/FindPageQuery.js.map +1 -1
  132. package/dist/esm/queries/QueryCache.d.ts +1 -1
  133. package/dist/esm/queries/QueryCache.js +4 -0
  134. package/dist/esm/queries/QueryCache.js.map +1 -1
  135. package/dist/esm/sync/FileSync.d.ts +23 -8
  136. package/dist/esm/sync/FileSync.js +76 -28
  137. package/dist/esm/sync/FileSync.js.map +1 -1
  138. package/dist/esm/sync/PresenceManager.d.ts +4 -3
  139. package/dist/esm/sync/PresenceManager.js +2 -2
  140. package/dist/esm/sync/PresenceManager.js.map +1 -1
  141. package/dist/esm/sync/PushPullSync.d.ts +4 -6
  142. package/dist/esm/sync/PushPullSync.js +13 -12
  143. package/dist/esm/sync/PushPullSync.js.map +1 -1
  144. package/dist/esm/sync/Sync.d.ts +9 -11
  145. package/dist/esm/sync/Sync.js +34 -29
  146. package/dist/esm/sync/Sync.js.map +1 -1
  147. package/dist/esm/sync/WebSocketSync.d.ts +4 -6
  148. package/dist/esm/sync/WebSocketSync.js +20 -22
  149. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  150. package/dist/esm/utils/Disposable.d.ts +5 -2
  151. package/dist/esm/utils/Disposable.js +3 -2
  152. package/dist/esm/utils/Disposable.js.map +1 -1
  153. package/dist/esm/utils/wip.d.ts +2 -0
  154. package/dist/esm/utils/wip.js +5 -0
  155. package/dist/esm/utils/wip.js.map +1 -0
  156. package/package.json +2 -2
  157. package/src/__tests__/batching.test.ts +6 -6
  158. package/src/__tests__/entities.test.ts +1 -1
  159. package/src/__tests__/fixtures/testStorage.ts +2 -10
  160. package/src/__tests__/queries.test.ts +1 -1
  161. package/src/backup.ts +3 -4
  162. package/src/client/Client.ts +69 -226
  163. package/src/client/ClientDescriptor.ts +53 -184
  164. package/src/context/Time.ts +35 -0
  165. package/src/context/context.ts +200 -0
  166. package/src/entities/DocumentManager.ts +0 -3
  167. package/src/entities/Entity.test.ts +9 -9
  168. package/src/entities/Entity.ts +6 -12
  169. package/src/entities/EntityCache.ts +0 -9
  170. package/src/entities/EntityMetadata.ts +4 -4
  171. package/src/entities/EntityStore.ts +26 -29
  172. package/src/entities/OperationBatcher.ts +9 -11
  173. package/src/entities/types.ts +1 -1
  174. package/src/errors.ts +13 -0
  175. package/src/files/EntityFile.ts +16 -5
  176. package/src/files/FileManager.ts +18 -245
  177. package/src/files/utils.ts +0 -15
  178. package/src/index.ts +2 -1
  179. package/src/{metadata → persistence}/MessageCreator.ts +46 -36
  180. package/src/persistence/PersistenceFiles.ts +227 -0
  181. package/src/persistence/PersistenceMetadata.ts +425 -0
  182. package/src/persistence/PersistenceQueries.ts +22 -0
  183. package/src/persistence/PersistenceRebaser.ts +171 -0
  184. package/src/{IDBService.ts → persistence/idb/IdbService.ts} +45 -12
  185. package/src/{files/FileStorage.ts → persistence/idb/files/IdbPersistenceFileDb.ts} +128 -86
  186. package/src/persistence/idb/idbPersistence.ts +116 -0
  187. package/src/persistence/idb/metadata/IdbMetadataDb.ts +460 -0
  188. package/src/{metadata → persistence/idb/metadata}/openMetadataDatabase.ts +21 -4
  189. package/src/persistence/idb/queries/IdbQueryDb.ts +251 -0
  190. package/src/{migration → persistence/idb/queries/migration}/db.ts +18 -72
  191. package/src/{migration → persistence/idb/queries/migration}/engine.ts +39 -62
  192. package/src/{migration → persistence/idb/queries/migration}/migrations.ts +13 -18
  193. package/src/{migration → persistence/idb/queries/migration}/openQueryDatabase.ts +5 -14
  194. package/src/{migration → persistence/idb/queries/migration}/paths.ts +4 -3
  195. package/src/persistence/idb/queries/migration/types.ts +8 -0
  196. package/src/persistence/idb/queries/ranges.ts +107 -0
  197. package/src/{idb.ts → persistence/idb/util.ts} +75 -0
  198. package/src/persistence/interfaces.ts +240 -0
  199. package/src/persistence/persistence.ts +223 -0
  200. package/src/queries/BaseQuery.ts +5 -1
  201. package/src/queries/CollectionQueries.ts +2 -2
  202. package/src/queries/FindAllQuery.ts +1 -3
  203. package/src/queries/FindInfiniteQuery.ts +2 -5
  204. package/src/queries/FindOneQuery.ts +1 -3
  205. package/src/queries/FindPageQuery.ts +1 -3
  206. package/src/queries/QueryCache.ts +20 -1
  207. package/src/sync/FileSync.ts +93 -30
  208. package/src/sync/PresenceManager.ts +5 -7
  209. package/src/sync/PushPullSync.ts +23 -19
  210. package/src/sync/Sync.ts +45 -36
  211. package/src/sync/WebSocketSync.ts +41 -27
  212. package/src/utils/Disposable.ts +7 -4
  213. package/src/utils/wip.ts +5 -0
  214. package/dist/esm/IDBService.js.map +0 -1
  215. package/dist/esm/__tests__/legacyOids.test.d.ts +0 -1
  216. package/dist/esm/__tests__/legacyOids.test.js +0 -352
  217. package/dist/esm/__tests__/legacyOids.test.js.map +0 -1
  218. package/dist/esm/context.d.ts +0 -45
  219. package/dist/esm/files/FileStorage.d.ts +0 -47
  220. package/dist/esm/files/FileStorage.js.map +0 -1
  221. package/dist/esm/idb.js.map +0 -1
  222. package/dist/esm/metadata/AckInfoStore.d.ts +0 -10
  223. package/dist/esm/metadata/AckInfoStore.js +0 -22
  224. package/dist/esm/metadata/AckInfoStore.js.map +0 -1
  225. package/dist/esm/metadata/BaselinesStore.d.ts +0 -40
  226. package/dist/esm/metadata/BaselinesStore.js +0 -102
  227. package/dist/esm/metadata/BaselinesStore.js.map +0 -1
  228. package/dist/esm/metadata/LocalReplicaStore.d.ts +0 -19
  229. package/dist/esm/metadata/LocalReplicaStore.js +0 -56
  230. package/dist/esm/metadata/LocalReplicaStore.js.map +0 -1
  231. package/dist/esm/metadata/MessageCreator.js.map +0 -1
  232. package/dist/esm/metadata/Metadata.d.ts +0 -146
  233. package/dist/esm/metadata/Metadata.js +0 -452
  234. package/dist/esm/metadata/Metadata.js.map +0 -1
  235. package/dist/esm/metadata/OperationsStore.d.ts +0 -62
  236. package/dist/esm/metadata/OperationsStore.js +0 -175
  237. package/dist/esm/metadata/OperationsStore.js.map +0 -1
  238. package/dist/esm/metadata/SchemaStore.d.ts +0 -9
  239. package/dist/esm/metadata/SchemaStore.js +0 -35
  240. package/dist/esm/metadata/SchemaStore.js.map +0 -1
  241. package/dist/esm/metadata/openMetadataDatabase.js.map +0 -1
  242. package/dist/esm/migration/db.js.map +0 -1
  243. package/dist/esm/migration/engine.d.ts +0 -15
  244. package/dist/esm/migration/engine.js.map +0 -1
  245. package/dist/esm/migration/errors.d.ts +0 -5
  246. package/dist/esm/migration/errors.js +0 -8
  247. package/dist/esm/migration/errors.js.map +0 -1
  248. package/dist/esm/migration/migrations.js.map +0 -1
  249. package/dist/esm/migration/openQueryDatabase.js.map +0 -1
  250. package/dist/esm/migration/openWIPDatabase.d.ts +0 -11
  251. package/dist/esm/migration/openWIPDatabase.js +0 -65
  252. package/dist/esm/migration/openWIPDatabase.js.map +0 -1
  253. package/dist/esm/migration/paths.js.map +0 -1
  254. package/dist/esm/migration/paths.test.js.map +0 -1
  255. package/dist/esm/migration/types.d.ts +0 -3
  256. package/dist/esm/migration/types.js.map +0 -1
  257. package/dist/esm/queries/QueryableStorage.d.ts +0 -20
  258. package/dist/esm/queries/QueryableStorage.js +0 -90
  259. package/dist/esm/queries/QueryableStorage.js.map +0 -1
  260. package/dist/esm/queries/dbQueries.d.ts +0 -22
  261. package/dist/esm/queries/dbQueries.js +0 -130
  262. package/dist/esm/queries/dbQueries.js.map +0 -1
  263. package/src/__tests__/legacyOids.test.ts +0 -375
  264. package/src/context.ts +0 -55
  265. package/src/metadata/AckInfoStore.ts +0 -30
  266. package/src/metadata/BaselinesStore.ts +0 -188
  267. package/src/metadata/LocalReplicaStore.ts +0 -79
  268. package/src/metadata/Metadata.ts +0 -685
  269. package/src/metadata/OperationsStore.ts +0 -332
  270. package/src/metadata/SchemaStore.ts +0 -47
  271. package/src/migration/errors.ts +0 -7
  272. package/src/migration/openWIPDatabase.ts +0 -97
  273. package/src/migration/types.ts +0 -4
  274. package/src/queries/QueryableStorage.ts +0 -122
  275. package/src/queries/dbQueries.ts +0 -161
  276. /package/dist/esm/{context.js → context/context.js} +0 -0
  277. /package/dist/esm/{migration → persistence/idb/queries/migration}/paths.d.ts +0 -0
  278. /package/dist/esm/{migration → persistence/idb/queries/migration}/paths.test.d.ts +0 -0
  279. /package/dist/esm/{migration → persistence/idb/queries/migration}/paths.test.js +0 -0
  280. /package/dist/esm/{migration → persistence/idb/queries/migration}/types.js +0 -0
  281. /package/src/{migration → persistence/idb/queries/migration}/paths.test.ts +0 -0
@@ -0,0 +1,171 @@
1
+ import {
2
+ applyPatch,
3
+ assignOid,
4
+ isFileRef,
5
+ ObjectIdentifier,
6
+ Ref,
7
+ } from '@verdant-web/common';
8
+ import { Context } from '../context/context.js';
9
+ import { AbstractTransaction, PersistenceMetadataDb } from './interfaces.js';
10
+
11
+ export class PersistenceRebaser {
12
+ constructor(
13
+ private db: PersistenceMetadataDb,
14
+ private ctx: Pick<
15
+ Context,
16
+ 'closing' | 'log' | 'time' | 'internalEvents' | 'globalEvents' | 'config'
17
+ >,
18
+ ) {}
19
+
20
+ /**
21
+ * Autonomous rebases are only allowed for clients who have never synced. They
22
+ * keep storage clean for non-syncing clients by compressing history.
23
+ */
24
+ tryAutonomousRebase = async () => {
25
+ const localReplicaInfo = await this.db.getLocalReplica();
26
+ if (localReplicaInfo.lastSyncedLogicalTime) return; // cannot autonomously rebase if we've synced
27
+ // but if we have never synced... we can rebase everything!
28
+ await this.runRebase(this.ctx.time.now);
29
+ };
30
+
31
+ /**
32
+ * Attempt to autonomously rebase local documents without server intervention.
33
+ * This can currently only happen for a client who has never synced before.
34
+ * The goal is to allow local-only clients to compress their history to exactly
35
+ * their undo stack.
36
+ */
37
+ private runRebase = async (globalAckTimestamp: string) => {
38
+ if (this.ctx.closing) return;
39
+
40
+ // find all operations before the global ack
41
+ let lastTimestamp;
42
+ const toRebase = new Set<ObjectIdentifier>();
43
+ const transaction = this.db.transaction({
44
+ storeNames: ['baselines', 'operations'],
45
+ mode: 'readwrite',
46
+ });
47
+ let operationCount = 0;
48
+ await this.db.iterateAllOperations(
49
+ (patch) => {
50
+ toRebase.add(patch.oid);
51
+ lastTimestamp = patch.timestamp;
52
+ operationCount++;
53
+ },
54
+ {
55
+ before: globalAckTimestamp,
56
+ transaction,
57
+ },
58
+ );
59
+
60
+ if (!toRebase.size) {
61
+ return;
62
+ }
63
+
64
+ if (this.ctx.closing) {
65
+ return;
66
+ }
67
+
68
+ // rebase each affected document
69
+ let newBaselines = [];
70
+ for (const oid of toRebase) {
71
+ newBaselines.push(
72
+ await this.rebase(
73
+ oid,
74
+ lastTimestamp || globalAckTimestamp,
75
+ transaction,
76
+ ),
77
+ );
78
+ }
79
+ this.ctx.globalEvents.emit('rebase');
80
+ };
81
+
82
+ /**
83
+ * Debounces rebase attempts to avoid thrashing the database with
84
+ * rebase operations.
85
+ */
86
+ scheduleRebase = async (timestamp: string) => {
87
+ if (this.rebaseTimeout) {
88
+ clearTimeout(this.rebaseTimeout);
89
+ }
90
+ this.rebaseTimeout = setTimeout(
91
+ this.runRebase,
92
+ this.ctx.config.persistence?.rebaseTimeout ?? 10000,
93
+ timestamp,
94
+ );
95
+ };
96
+ private rebaseTimeout: NodeJS.Timeout | null = null;
97
+
98
+ rebase = async (
99
+ oid: ObjectIdentifier,
100
+ upTo: string,
101
+ providedTx?: AbstractTransaction,
102
+ ) => {
103
+ const transaction =
104
+ providedTx ||
105
+ this.db.transaction({
106
+ storeNames: ['operations', 'baselines'],
107
+ mode: 'readwrite',
108
+ });
109
+ const baseline = await this.db.getBaseline(oid, { transaction });
110
+ let current: any = baseline?.snapshot || undefined;
111
+ let operationsApplied = 0;
112
+ let authz = baseline?.authz;
113
+ const deletedRefs: Ref[] = [];
114
+ await this.db.consumeEntityOperations(
115
+ oid,
116
+ (patch) => {
117
+ // FIXME: this seems like the wrong place to do this
118
+ // but it's here as a safety measure...
119
+ if (!baseline || patch.timestamp > baseline.timestamp) {
120
+ current = applyPatch(current, patch.data, deletedRefs);
121
+ if (patch.data.op === 'initialize') {
122
+ authz = patch.authz;
123
+ }
124
+ }
125
+ // delete all prior operations to the baseline
126
+ operationsApplied++;
127
+ },
128
+ {
129
+ to: upTo,
130
+ transaction,
131
+ },
132
+ );
133
+ if (current) {
134
+ assignOid(current, oid);
135
+ }
136
+ const newBaseline = {
137
+ oid,
138
+ snapshot: current,
139
+ timestamp: upTo,
140
+ authz,
141
+ };
142
+ if (newBaseline.snapshot) {
143
+ await this.db.setBaselines([newBaseline], { transaction });
144
+ } else {
145
+ await this.db.deleteBaseline(oid, { transaction });
146
+ }
147
+
148
+ this.ctx.log(
149
+ 'debug',
150
+ 'rebased',
151
+ oid,
152
+ 'up to',
153
+ upTo,
154
+ ':',
155
+ current,
156
+ 'and deleted',
157
+ operationsApplied,
158
+ 'operations',
159
+ );
160
+
161
+ // cleanup deleted refs
162
+ if (deletedRefs.length) {
163
+ const fileRefs = deletedRefs.filter(isFileRef);
164
+ if (fileRefs.length) {
165
+ this.ctx.internalEvents.emit('filesDeleted', fileRefs);
166
+ }
167
+ }
168
+
169
+ return newBaseline;
170
+ };
171
+ }
@@ -1,12 +1,13 @@
1
- import { Context } from './context.js';
1
+ import { Context } from '../../context/context.js';
2
2
  import {
3
+ copyDatabase,
3
4
  createAbortableTransaction,
4
5
  isAbortError,
5
6
  storeRequestPromise,
6
- } from './idb.js';
7
- import { Disposable } from './utils/Disposable.js';
7
+ } from './util.js';
8
+ import { Disposable } from '../../utils/Disposable.js';
8
9
 
9
- export class IDBService extends Disposable {
10
+ export class IdbService extends Disposable {
10
11
  protected log?: Context['log'];
11
12
  private globalAbortController = new AbortController();
12
13
 
@@ -19,6 +20,10 @@ export class IDBService extends Disposable {
19
20
  this.addDispose(() => {
20
21
  this.globalAbortController.abort();
21
22
  });
23
+ this.db.addEventListener('versionchange', this.onVersionChange);
24
+ this.addDispose(() => {
25
+ this.db.removeEventListener('versionchange', this.onVersionChange);
26
+ });
22
27
  }
23
28
 
24
29
  createTransaction = (
@@ -45,7 +50,7 @@ export class IDBService extends Disposable {
45
50
  return tx;
46
51
  };
47
52
 
48
- run = async <T>(
53
+ run = async <T = any>(
49
54
  storeName: string,
50
55
  getRequest: (store: IDBObjectStore) => IDBRequest<T>,
51
56
  opts?: {
@@ -66,7 +71,7 @@ export class IDBService extends Disposable {
66
71
  storeName: string,
67
72
  getRequests: (store: IDBObjectStore) => IDBRequest<T>[],
68
73
  opts?: {
69
- mode: 'readonly' | 'readwrite';
74
+ mode?: 'readonly' | 'readwrite';
70
75
  transaction?: IDBTransaction;
71
76
  abort?: AbortSignal;
72
77
  },
@@ -80,8 +85,16 @@ export class IDBService extends Disposable {
80
85
 
81
86
  iterate = async <T>(
82
87
  storeName: string,
83
- getRequest: (store: IDBObjectStore) => IDBRequest | IDBRequest[],
84
- iterator: (value: T, store: IDBObjectStore) => boolean | void,
88
+ getRequest: (
89
+ store: IDBObjectStore,
90
+ ) =>
91
+ | IDBRequest<IDBCursorWithValue | null>
92
+ | IDBRequest<IDBCursorWithValue | null>[],
93
+ iterator: (
94
+ value: T,
95
+ store: IDBObjectStore,
96
+ cursor: IDBCursorWithValue,
97
+ ) => boolean | void,
85
98
  opts?: {
86
99
  mode?: 'readonly' | 'readwrite';
87
100
  transaction?: IDBTransaction;
@@ -98,8 +111,12 @@ export class IDBService extends Disposable {
98
111
  req.onsuccess = () => {
99
112
  const cursor = req.result;
100
113
  if (cursor) {
101
- iterator(cursor.value, store);
102
- cursor.continue();
114
+ const stop = iterator(cursor.value, store, cursor);
115
+ if (stop) {
116
+ resolve();
117
+ } else {
118
+ cursor.continue();
119
+ }
103
120
  } else {
104
121
  resolve();
105
122
  }
@@ -119,7 +136,7 @@ export class IDBService extends Disposable {
119
136
  request.onsuccess = () => {
120
137
  const cursor = request.result as IDBCursorWithValue | null;
121
138
  if (cursor) {
122
- const stop = iterator(cursor.value, store);
139
+ const stop = iterator(cursor.value, store, cursor);
123
140
  if (stop) {
124
141
  resolve();
125
142
  } else {
@@ -139,9 +156,25 @@ export class IDBService extends Disposable {
139
156
  });
140
157
  };
141
158
 
142
- clear = (storeName: string) => {
159
+ clear = (storeName: string, transaction?: IDBTransaction) => {
143
160
  return this.run<undefined>(storeName, (store) => store.clear(), {
144
161
  mode: 'readwrite',
162
+ transaction,
145
163
  });
146
164
  };
165
+
166
+ cloneTo = async (otherDb: IDBDatabase) => {
167
+ await copyDatabase(this.db, otherDb);
168
+ };
169
+
170
+ private onVersionChange = () => {
171
+ this.log?.(
172
+ 'warn',
173
+ `Another tab has requested a version change for ${this.db.name}`,
174
+ );
175
+ this.db.close();
176
+ if (typeof window !== 'undefined') {
177
+ window.location.reload();
178
+ }
179
+ };
147
180
  }
@@ -1,7 +1,12 @@
1
1
  import { FileData } from '@verdant-web/common';
2
- import { IDBService } from '../IDBService.js';
3
- import { fileToArrayBuffer } from './utils.js';
4
- import { getAllFromObjectStores, getSizeOfObjectStore } from '../idb.js';
2
+ import {
3
+ AbstractTransaction,
4
+ PersistedFileData,
5
+ PersistenceFileDb,
6
+ QueryMode,
7
+ } from '../../interfaces.js';
8
+ import { IdbService } from '../IdbService.js';
9
+ import { getAllFromObjectStores, getSizeOfObjectStore } from '../util.js';
5
10
 
6
11
  /**
7
12
  * When stored in IDB, replace the file blob with an array buffer
@@ -15,20 +20,27 @@ export interface StoredFileData extends Omit<FileData, 'remote' | 'file'> {
15
20
  timestamp?: string;
16
21
  }
17
22
 
18
- export interface ReturnedFileData extends FileData {
19
- deletedAt: number | null;
20
- }
23
+ export class IdbPersistenceFileDb
24
+ extends IdbService
25
+ implements PersistenceFileDb
26
+ {
27
+ transaction = (opts: {
28
+ mode?: QueryMode;
29
+ storeNames: string[];
30
+ abort?: AbortSignal;
31
+ }): AbstractTransaction => {
32
+ return this.createTransaction(opts.storeNames, {
33
+ mode: opts.mode,
34
+ abort: opts.abort,
35
+ });
36
+ };
21
37
 
22
- export class FileStorage extends IDBService {
23
- addFile = async (
38
+ add = async (
24
39
  file: FileData,
25
- {
26
- transaction,
27
- downloadRemote = false,
28
- }: { transaction?: IDBTransaction; downloadRemote?: boolean } = {},
29
- ) => {
40
+ options?: { transaction?: AbstractTransaction; downloadRemote?: boolean },
41
+ ): Promise<void> => {
30
42
  let buffer = file.file ? await fileToArrayBuffer(file.file) : undefined;
31
- if (!buffer && downloadRemote && file.url) {
43
+ if (!buffer && options?.downloadRemote && file.url) {
32
44
  try {
33
45
  buffer = await fetch(file.url, {
34
46
  method: 'GET',
@@ -41,7 +53,7 @@ export class FileStorage extends IDBService {
41
53
  );
42
54
  }
43
55
  }
44
- return this.run(
56
+ await this.run(
45
57
  'files',
46
58
  (store) => {
47
59
  return store.put({
@@ -53,33 +65,25 @@ export class FileStorage extends IDBService {
53
65
  type: file.type,
54
66
  url: file.url,
55
67
  buffer,
56
- } as StoredFileData);
68
+ } satisfies StoredFileData);
69
+ },
70
+ {
71
+ mode: 'readwrite',
72
+ transaction: options?.transaction as IDBTransaction,
57
73
  },
58
- { mode: 'readwrite', transaction },
59
74
  );
60
75
  };
61
-
62
- private hydrateFileData = (raw: StoredFileData): ReturnedFileData => {
63
- (raw as any).remote = raw.remote === 'true';
64
- const buffer = raw.buffer;
65
- delete raw.buffer;
66
- (raw as unknown as FileData).file = buffer
67
- ? arrayBufferToBlob(buffer, raw.type)
68
- : undefined;
69
- return raw as unknown as ReturnedFileData;
70
- };
71
-
72
76
  markUploaded = async (
73
77
  id: string,
74
- { transaction }: { transaction?: IDBTransaction } = {},
75
- ) => {
76
- const current = await this.getFileRaw(id, { transaction });
78
+ options?: { transaction?: AbstractTransaction },
79
+ ): Promise<void> => {
80
+ const current = await this.getFileRaw(id, options);
77
81
 
78
82
  if (!current) {
79
83
  throw new Error('File is not in local database');
80
84
  }
81
85
 
82
- return this.run(
86
+ await this.run(
83
87
  'files',
84
88
  (store) => {
85
89
  return store.put({
@@ -87,62 +91,48 @@ export class FileStorage extends IDBService {
87
91
  remote: 'true',
88
92
  } as StoredFileData);
89
93
  },
90
- { mode: 'readwrite', transaction },
91
- );
92
- };
93
-
94
- private getFileRaw = async (
95
- id: string,
96
- { transaction }: { transaction?: IDBTransaction } = {},
97
- ): Promise<StoredFileData | undefined> => {
98
- const raw = await this.run<StoredFileData>(
99
- 'files',
100
- (store) => {
101
- return store.get(id);
94
+ {
95
+ mode: 'readwrite',
96
+ transaction: options?.transaction as IDBTransaction,
102
97
  },
103
- { mode: 'readonly', transaction },
104
98
  );
105
- if (!raw) {
106
- return undefined;
107
- }
108
- return raw;
109
99
  };
110
-
111
- getFile = async (
112
- id: string,
113
- { transaction }: { transaction?: IDBTransaction } = {},
114
- ): Promise<ReturnedFileData | undefined> => {
115
- const raw = await this.getFileRaw(id, { transaction });
100
+ get = async (
101
+ fileId: string,
102
+ options?: { transaction?: AbstractTransaction },
103
+ ): Promise<PersistedFileData | null> => {
104
+ const raw = await this.getFileRaw(fileId, options);
116
105
  if (!raw) {
117
- return undefined;
106
+ return null;
118
107
  }
119
108
  return this.hydrateFileData(raw);
120
109
  };
121
-
122
- deleteFile(
123
- id: string,
124
- { transaction }: { transaction?: IDBTransaction } = {},
125
- ) {
110
+ delete = (
111
+ fileId: string,
112
+ options?: { transaction?: AbstractTransaction },
113
+ ): Promise<void> => {
126
114
  return this.run<undefined>(
127
115
  'files',
128
116
  (store) => {
129
- return store.delete(id);
117
+ return store.delete(fileId);
118
+ },
119
+ {
120
+ mode: 'readwrite',
121
+ transaction: options?.transaction as IDBTransaction,
130
122
  },
131
- { mode: 'readwrite', transaction },
132
123
  );
133
- }
134
-
124
+ };
135
125
  markPendingDelete = async (
136
- id: string,
137
- { transaction }: { transaction?: IDBTransaction } = {},
138
- ) => {
139
- const current = await this.getFileRaw(id, { transaction });
126
+ fileId: string,
127
+ options?: { transaction?: AbstractTransaction },
128
+ ): Promise<void> => {
129
+ const current = await this.getFileRaw(fileId, options);
140
130
 
141
131
  if (!current) {
142
132
  throw new Error('File is not in local database');
143
133
  }
144
134
 
145
- return this.run(
135
+ await this.run(
146
136
  'files',
147
137
  (store) => {
148
138
  return store.put({
@@ -150,23 +140,31 @@ export class FileStorage extends IDBService {
150
140
  deletedAt: Date.now(),
151
141
  } as StoredFileData);
152
142
  },
153
- { mode: 'readwrite', transaction },
143
+ {
144
+ mode: 'readwrite',
145
+ transaction: options?.transaction as IDBTransaction,
146
+ },
154
147
  );
155
148
  };
156
-
157
- listUnsynced = async () => {
149
+ listUnsynced = async (options?: {
150
+ transaction?: AbstractTransaction;
151
+ }): Promise<PersistedFileData[]> => {
158
152
  const raw = await this.run<StoredFileData[]>(
159
153
  'files',
160
154
  (store) => {
161
155
  return store.index('remote').getAll('false');
162
156
  },
163
- { mode: 'readonly' },
157
+ { mode: 'readonly', transaction: options?.transaction as IDBTransaction },
164
158
  );
165
159
  return raw.map(this.hydrateFileData);
166
160
  };
167
-
168
- resetSyncedStatusSince = async (since: string | null) => {
169
- const tx = this.createTransaction(['files'], { mode: 'readwrite' });
161
+ resetSyncedStatusSince = async (
162
+ since: string | null,
163
+ options?: { transaction?: AbstractTransaction },
164
+ ): Promise<void> => {
165
+ const tx: IDBTransaction =
166
+ (options?.transaction as any) ??
167
+ this.createTransaction(['files'], { mode: 'readwrite' });
170
168
  const raw = await this.run<StoredFileData[]>(
171
169
  'files',
172
170
  (store) => {
@@ -194,11 +192,10 @@ export class FileStorage extends IDBService {
194
192
  }),
195
193
  );
196
194
  };
197
-
198
195
  iterateOverPendingDelete = (
199
- iterator: (file: ReturnedFileData, store: IDBObjectStore) => void,
200
- transaction?: IDBTransaction,
201
- ) => {
196
+ iterator: (file: PersistedFileData, store: IDBObjectStore) => void,
197
+ options?: { transaction?: IDBTransaction },
198
+ ): Promise<void> => {
202
199
  return this.iterate<StoredFileData>(
203
200
  'files',
204
201
  (store) => {
@@ -209,22 +206,67 @@ export class FileStorage extends IDBService {
209
206
  (value, store) => {
210
207
  iterator(this.hydrateFileData(value), store);
211
208
  },
212
- { mode: 'readwrite', transaction },
209
+ {
210
+ mode: 'readwrite',
211
+ transaction: options?.transaction as IDBTransaction,
212
+ },
213
213
  );
214
214
  };
215
-
216
- getAll = async () => {
215
+ getAll = async (options?: {
216
+ transaction?: AbstractTransaction;
217
+ }): Promise<PersistedFileData[]> => {
217
218
  const [files] = await getAllFromObjectStores(this.db, ['files']);
218
219
  return files.map(this.hydrateFileData);
219
220
  };
220
-
221
- stats = async () => {
221
+ stats = async (): Promise<{ size: { count: number; size: number } }> => {
222
222
  return {
223
223
  size: await getSizeOfObjectStore(this.db, 'files'),
224
224
  };
225
225
  };
226
+
227
+ private hydrateFileData = (raw: StoredFileData): PersistedFileData => {
228
+ (raw as any).remote = raw.remote === 'true';
229
+ const buffer = raw.buffer;
230
+ delete raw.buffer;
231
+ (raw as unknown as FileData).file = buffer
232
+ ? arrayBufferToBlob(buffer, raw.type)
233
+ : undefined;
234
+ return raw as unknown as PersistedFileData;
235
+ };
236
+
237
+ private getFileRaw = async (
238
+ id: string,
239
+ { transaction }: { transaction?: AbstractTransaction } = {},
240
+ ): Promise<StoredFileData | undefined> => {
241
+ const raw = await this.run<StoredFileData>(
242
+ 'files',
243
+ (store) => {
244
+ return store.get(id);
245
+ },
246
+ { mode: 'readonly', transaction: transaction as IDBTransaction },
247
+ );
248
+ if (!raw) {
249
+ return undefined;
250
+ }
251
+ return raw;
252
+ };
226
253
  }
227
254
 
228
255
  export function arrayBufferToBlob(buffer: ArrayBuffer, type: string) {
229
256
  return new Blob([buffer], { type });
230
257
  }
258
+
259
+ function fileToArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
260
+ // special case for testing...
261
+ if ('__testReadBuffer' in file) {
262
+ return Promise.resolve<any>(file.__testReadBuffer);
263
+ }
264
+ return new Promise<ArrayBuffer>((resolve, reject) => {
265
+ const reader = new FileReader();
266
+ reader.onload = () => {
267
+ resolve(reader.result as ArrayBuffer);
268
+ };
269
+ reader.onerror = reject;
270
+ reader.readAsArrayBuffer(file);
271
+ });
272
+ }