@verdant-web/store 2.8.4 → 3.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 (307) hide show
  1. package/dist/bundle/index.js +9 -10
  2. package/dist/bundle/index.js.map +4 -4
  3. package/dist/cjs/DocumentManager.d.ts +6 -5
  4. package/dist/cjs/DocumentManager.js +2 -2
  5. package/dist/cjs/DocumentManager.js.map +1 -1
  6. package/dist/cjs/IDBService.d.ts +28 -7
  7. package/dist/cjs/IDBService.js +50 -13
  8. package/dist/cjs/IDBService.js.map +1 -1
  9. package/dist/cjs/UndoHistory.d.ts +1 -1
  10. package/dist/cjs/UndoHistory.js +6 -2
  11. package/dist/cjs/UndoHistory.js.map +1 -1
  12. package/dist/cjs/__tests__/batching.test.js +3 -1
  13. package/dist/cjs/__tests__/batching.test.js.map +1 -1
  14. package/dist/cjs/__tests__/documents.test.js +54 -6
  15. package/dist/cjs/__tests__/documents.test.js.map +1 -1
  16. package/dist/cjs/__tests__/fixtures/testStorage.d.ts +8 -2
  17. package/dist/cjs/__tests__/fixtures/testStorage.js +8 -1
  18. package/dist/cjs/__tests__/fixtures/testStorage.js.map +1 -1
  19. package/dist/cjs/__tests__/legacyOids.test.js +50 -17
  20. package/dist/cjs/__tests__/legacyOids.test.js.map +1 -1
  21. package/dist/cjs/__tests__/mutations.test.js +9 -3
  22. package/dist/cjs/__tests__/mutations.test.js.map +1 -1
  23. package/dist/cjs/__tests__/queries.test.js +6 -2
  24. package/dist/cjs/__tests__/queries.test.js.map +1 -1
  25. package/dist/cjs/__tests__/setup/indexedDB.d.ts +1 -1
  26. package/dist/cjs/__tests__/setup/indexedDB.js +13 -1
  27. package/dist/cjs/__tests__/setup/indexedDB.js.map +1 -1
  28. package/dist/cjs/__tests__/undo.test.js +16 -9
  29. package/dist/cjs/__tests__/undo.test.js.map +1 -1
  30. package/dist/cjs/client/Client.d.ts +2 -3
  31. package/dist/cjs/client/Client.js +8 -4
  32. package/dist/cjs/client/Client.js.map +1 -1
  33. package/dist/cjs/client/ClientDescriptor.js +21 -6
  34. package/dist/cjs/client/ClientDescriptor.js.map +1 -1
  35. package/dist/cjs/context.d.ts +10 -1
  36. package/dist/cjs/entities/2/Entity.d.ts +148 -0
  37. package/dist/cjs/entities/2/Entity.js +711 -0
  38. package/dist/cjs/entities/2/Entity.js.map +1 -0
  39. package/dist/cjs/entities/2/Entity.test.d.ts +1 -0
  40. package/dist/cjs/entities/2/Entity.test.js +194 -0
  41. package/dist/cjs/entities/2/Entity.test.js.map +1 -0
  42. package/dist/cjs/entities/2/EntityCache.d.ts +15 -0
  43. package/dist/cjs/entities/2/EntityCache.js +39 -0
  44. package/dist/cjs/entities/2/EntityCache.js.map +1 -0
  45. package/dist/cjs/entities/2/EntityMetadata.d.ts +68 -0
  46. package/dist/cjs/entities/2/EntityMetadata.js +261 -0
  47. package/dist/cjs/entities/2/EntityMetadata.js.map +1 -0
  48. package/dist/cjs/entities/2/EntityStore.d.ts +78 -0
  49. package/dist/cjs/entities/2/EntityStore.js +352 -0
  50. package/dist/cjs/entities/2/EntityStore.js.map +1 -0
  51. package/dist/cjs/entities/2/OperationBatcher.d.ts +52 -0
  52. package/dist/cjs/entities/2/OperationBatcher.js +165 -0
  53. package/dist/cjs/entities/2/OperationBatcher.js.map +1 -0
  54. package/dist/cjs/entities/2/types.d.ts +84 -0
  55. package/dist/cjs/entities/2/types.js +3 -0
  56. package/dist/cjs/entities/2/types.js.map +1 -0
  57. package/dist/cjs/entities/Entity.d.ts +0 -7
  58. package/dist/cjs/entities/Entity.js +7 -0
  59. package/dist/cjs/entities/Entity.js.map +1 -1
  60. package/dist/cjs/entities/EntityStore.js +4 -20
  61. package/dist/cjs/entities/EntityStore.js.map +1 -1
  62. package/dist/cjs/entities/FakeWeakRef.d.ts +11 -0
  63. package/dist/cjs/entities/FakeWeakRef.js +19 -0
  64. package/dist/cjs/entities/FakeWeakRef.js.map +1 -0
  65. package/dist/cjs/files/EntityFile.d.ts +5 -2
  66. package/dist/cjs/files/EntityFile.js +8 -4
  67. package/dist/cjs/files/EntityFile.js.map +1 -1
  68. package/dist/cjs/files/FileManager.d.ts +3 -1
  69. package/dist/cjs/files/FileManager.js +5 -3
  70. package/dist/cjs/files/FileManager.js.map +1 -1
  71. package/dist/cjs/files/FileStorage.js +7 -7
  72. package/dist/cjs/files/FileStorage.js.map +1 -1
  73. package/dist/cjs/files/utils.d.ts +2 -0
  74. package/dist/cjs/files/utils.js +8 -2
  75. package/dist/cjs/files/utils.js.map +1 -1
  76. package/dist/cjs/idb.d.ts +2 -0
  77. package/dist/cjs/idb.js +50 -4
  78. package/dist/cjs/idb.js.map +1 -1
  79. package/dist/cjs/index.d.ts +2 -2
  80. package/dist/cjs/index.js +1 -1
  81. package/dist/cjs/index.js.map +1 -1
  82. package/dist/cjs/indexes.d.ts +3 -0
  83. package/dist/cjs/indexes.js +20 -0
  84. package/dist/cjs/indexes.js.map +1 -0
  85. package/dist/cjs/metadata/AckInfoStore.js +1 -1
  86. package/dist/cjs/metadata/AckInfoStore.js.map +1 -1
  87. package/dist/cjs/metadata/BaselinesStore.d.ts +4 -1
  88. package/dist/cjs/metadata/BaselinesStore.js +19 -10
  89. package/dist/cjs/metadata/BaselinesStore.js.map +1 -1
  90. package/dist/cjs/metadata/LocalReplicaStore.d.ts +1 -1
  91. package/dist/cjs/metadata/LocalReplicaStore.js +11 -5
  92. package/dist/cjs/metadata/LocalReplicaStore.js.map +1 -1
  93. package/dist/cjs/metadata/Metadata.d.ts +26 -5
  94. package/dist/cjs/metadata/Metadata.js +55 -18
  95. package/dist/cjs/metadata/Metadata.js.map +1 -1
  96. package/dist/cjs/metadata/OperationsStore.d.ts +3 -0
  97. package/dist/cjs/metadata/OperationsStore.js +35 -15
  98. package/dist/cjs/metadata/OperationsStore.js.map +1 -1
  99. package/dist/cjs/migration/openDatabase.js +31 -10
  100. package/dist/cjs/migration/openDatabase.js.map +1 -1
  101. package/dist/cjs/queries/BaseQuery.js +14 -2
  102. package/dist/cjs/queries/BaseQuery.js.map +1 -1
  103. package/dist/cjs/queries/CollectionQueries.d.ts +2 -4
  104. package/dist/cjs/queries/CollectionQueries.js +1 -1
  105. package/dist/cjs/queries/CollectionQueries.js.map +1 -1
  106. package/dist/cjs/queries/FindAllQuery.js +1 -0
  107. package/dist/cjs/queries/FindAllQuery.js.map +1 -1
  108. package/dist/cjs/queries/QueryCache.d.ts +1 -0
  109. package/dist/cjs/queries/QueryCache.js +4 -0
  110. package/dist/cjs/queries/QueryCache.js.map +1 -1
  111. package/dist/cjs/queries/QueryableStorage.d.ts +20 -0
  112. package/dist/cjs/queries/QueryableStorage.js +84 -0
  113. package/dist/cjs/queries/QueryableStorage.js.map +1 -0
  114. package/dist/cjs/queries/dbQueries.js +13 -3
  115. package/dist/cjs/queries/dbQueries.js.map +1 -1
  116. package/dist/cjs/queries/utils.js +1 -1
  117. package/dist/cjs/queries/utils.js.map +1 -1
  118. package/dist/cjs/sync/FileSync.d.ts +1 -0
  119. package/dist/cjs/sync/FileSync.js +1 -0
  120. package/dist/cjs/sync/FileSync.js.map +1 -1
  121. package/dist/cjs/sync/PushPullSync.d.ts +2 -1
  122. package/dist/cjs/sync/PushPullSync.js +7 -1
  123. package/dist/cjs/sync/PushPullSync.js.map +1 -1
  124. package/dist/cjs/sync/Sync.d.ts +6 -3
  125. package/dist/cjs/sync/Sync.js +9 -4
  126. package/dist/cjs/sync/Sync.js.map +1 -1
  127. package/dist/cjs/sync/WebSocketSync.d.ts +4 -1
  128. package/dist/cjs/sync/WebSocketSync.js +41 -11
  129. package/dist/cjs/sync/WebSocketSync.js.map +1 -1
  130. package/dist/esm/DocumentManager.d.ts +6 -5
  131. package/dist/esm/DocumentManager.js +2 -2
  132. package/dist/esm/DocumentManager.js.map +1 -1
  133. package/dist/esm/IDBService.d.ts +28 -7
  134. package/dist/esm/IDBService.js +51 -14
  135. package/dist/esm/IDBService.js.map +1 -1
  136. package/dist/esm/UndoHistory.d.ts +1 -1
  137. package/dist/esm/UndoHistory.js +6 -2
  138. package/dist/esm/UndoHistory.js.map +1 -1
  139. package/dist/esm/__tests__/batching.test.js +3 -1
  140. package/dist/esm/__tests__/batching.test.js.map +1 -1
  141. package/dist/esm/__tests__/documents.test.js +54 -6
  142. package/dist/esm/__tests__/documents.test.js.map +1 -1
  143. package/dist/esm/__tests__/fixtures/testStorage.d.ts +8 -2
  144. package/dist/esm/__tests__/fixtures/testStorage.js +8 -1
  145. package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
  146. package/dist/esm/__tests__/legacyOids.test.js +50 -17
  147. package/dist/esm/__tests__/legacyOids.test.js.map +1 -1
  148. package/dist/esm/__tests__/mutations.test.js +9 -3
  149. package/dist/esm/__tests__/mutations.test.js.map +1 -1
  150. package/dist/esm/__tests__/queries.test.js +6 -2
  151. package/dist/esm/__tests__/queries.test.js.map +1 -1
  152. package/dist/esm/__tests__/setup/indexedDB.d.ts +1 -1
  153. package/dist/esm/__tests__/setup/indexedDB.js +13 -1
  154. package/dist/esm/__tests__/setup/indexedDB.js.map +1 -1
  155. package/dist/esm/__tests__/undo.test.js +16 -9
  156. package/dist/esm/__tests__/undo.test.js.map +1 -1
  157. package/dist/esm/client/Client.d.ts +2 -3
  158. package/dist/esm/client/Client.js +8 -4
  159. package/dist/esm/client/Client.js.map +1 -1
  160. package/dist/esm/client/ClientDescriptor.js +21 -6
  161. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  162. package/dist/esm/context.d.ts +10 -1
  163. package/dist/esm/entities/2/Entity.d.ts +148 -0
  164. package/dist/esm/entities/2/Entity.js +707 -0
  165. package/dist/esm/entities/2/Entity.js.map +1 -0
  166. package/dist/esm/entities/2/Entity.test.d.ts +1 -0
  167. package/dist/esm/entities/2/Entity.test.js +192 -0
  168. package/dist/esm/entities/2/Entity.test.js.map +1 -0
  169. package/dist/esm/entities/2/EntityCache.d.ts +15 -0
  170. package/dist/esm/entities/2/EntityCache.js +35 -0
  171. package/dist/esm/entities/2/EntityCache.js.map +1 -0
  172. package/dist/esm/entities/2/EntityMetadata.d.ts +68 -0
  173. package/dist/esm/entities/2/EntityMetadata.js +256 -0
  174. package/dist/esm/entities/2/EntityMetadata.js.map +1 -0
  175. package/dist/esm/entities/2/EntityStore.d.ts +78 -0
  176. package/dist/esm/entities/2/EntityStore.js +348 -0
  177. package/dist/esm/entities/2/EntityStore.js.map +1 -0
  178. package/dist/esm/entities/2/OperationBatcher.d.ts +52 -0
  179. package/dist/esm/entities/2/OperationBatcher.js +161 -0
  180. package/dist/esm/entities/2/OperationBatcher.js.map +1 -0
  181. package/dist/esm/entities/2/types.d.ts +84 -0
  182. package/dist/esm/entities/2/types.js +2 -0
  183. package/dist/esm/entities/2/types.js.map +1 -0
  184. package/dist/esm/entities/Entity.d.ts +0 -7
  185. package/dist/esm/entities/Entity.js +7 -0
  186. package/dist/esm/entities/Entity.js.map +1 -1
  187. package/dist/esm/entities/EntityStore.js +4 -20
  188. package/dist/esm/entities/EntityStore.js.map +1 -1
  189. package/dist/esm/entities/FakeWeakRef.d.ts +11 -0
  190. package/dist/esm/entities/FakeWeakRef.js +15 -0
  191. package/dist/esm/entities/FakeWeakRef.js.map +1 -0
  192. package/dist/esm/files/EntityFile.d.ts +5 -2
  193. package/dist/esm/files/EntityFile.js +8 -4
  194. package/dist/esm/files/EntityFile.js.map +1 -1
  195. package/dist/esm/files/FileManager.d.ts +3 -1
  196. package/dist/esm/files/FileManager.js +5 -3
  197. package/dist/esm/files/FileManager.js.map +1 -1
  198. package/dist/esm/files/FileStorage.js +7 -7
  199. package/dist/esm/files/FileStorage.js.map +1 -1
  200. package/dist/esm/files/utils.d.ts +2 -0
  201. package/dist/esm/files/utils.js +6 -1
  202. package/dist/esm/files/utils.js.map +1 -1
  203. package/dist/esm/idb.d.ts +2 -0
  204. package/dist/esm/idb.js +47 -3
  205. package/dist/esm/idb.js.map +1 -1
  206. package/dist/esm/index.d.ts +2 -2
  207. package/dist/esm/index.js +1 -1
  208. package/dist/esm/index.js.map +1 -1
  209. package/dist/esm/indexes.d.ts +3 -0
  210. package/dist/esm/indexes.js +15 -0
  211. package/dist/esm/indexes.js.map +1 -0
  212. package/dist/esm/metadata/AckInfoStore.js +1 -1
  213. package/dist/esm/metadata/AckInfoStore.js.map +1 -1
  214. package/dist/esm/metadata/BaselinesStore.d.ts +4 -1
  215. package/dist/esm/metadata/BaselinesStore.js +19 -10
  216. package/dist/esm/metadata/BaselinesStore.js.map +1 -1
  217. package/dist/esm/metadata/LocalReplicaStore.d.ts +1 -1
  218. package/dist/esm/metadata/LocalReplicaStore.js +11 -5
  219. package/dist/esm/metadata/LocalReplicaStore.js.map +1 -1
  220. package/dist/esm/metadata/Metadata.d.ts +26 -5
  221. package/dist/esm/metadata/Metadata.js +56 -19
  222. package/dist/esm/metadata/Metadata.js.map +1 -1
  223. package/dist/esm/metadata/OperationsStore.d.ts +3 -0
  224. package/dist/esm/metadata/OperationsStore.js +35 -15
  225. package/dist/esm/metadata/OperationsStore.js.map +1 -1
  226. package/dist/esm/migration/openDatabase.js +32 -11
  227. package/dist/esm/migration/openDatabase.js.map +1 -1
  228. package/dist/esm/queries/BaseQuery.js +14 -2
  229. package/dist/esm/queries/BaseQuery.js.map +1 -1
  230. package/dist/esm/queries/CollectionQueries.d.ts +2 -4
  231. package/dist/esm/queries/CollectionQueries.js +1 -1
  232. package/dist/esm/queries/CollectionQueries.js.map +1 -1
  233. package/dist/esm/queries/FindAllQuery.js +1 -0
  234. package/dist/esm/queries/FindAllQuery.js.map +1 -1
  235. package/dist/esm/queries/QueryCache.d.ts +1 -0
  236. package/dist/esm/queries/QueryCache.js +4 -0
  237. package/dist/esm/queries/QueryCache.js.map +1 -1
  238. package/dist/esm/queries/QueryableStorage.d.ts +20 -0
  239. package/dist/esm/queries/QueryableStorage.js +80 -0
  240. package/dist/esm/queries/QueryableStorage.js.map +1 -0
  241. package/dist/esm/queries/dbQueries.js +13 -3
  242. package/dist/esm/queries/dbQueries.js.map +1 -1
  243. package/dist/esm/queries/utils.js +1 -1
  244. package/dist/esm/queries/utils.js.map +1 -1
  245. package/dist/esm/sync/FileSync.d.ts +1 -0
  246. package/dist/esm/sync/FileSync.js +1 -0
  247. package/dist/esm/sync/FileSync.js.map +1 -1
  248. package/dist/esm/sync/PushPullSync.d.ts +2 -1
  249. package/dist/esm/sync/PushPullSync.js +7 -1
  250. package/dist/esm/sync/PushPullSync.js.map +1 -1
  251. package/dist/esm/sync/Sync.d.ts +6 -3
  252. package/dist/esm/sync/Sync.js +9 -4
  253. package/dist/esm/sync/Sync.js.map +1 -1
  254. package/dist/esm/sync/WebSocketSync.d.ts +4 -1
  255. package/dist/esm/sync/WebSocketSync.js +41 -11
  256. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  257. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  258. package/dist/tsconfig.tsbuildinfo +1 -1
  259. package/package.json +8 -7
  260. package/src/DocumentManager.ts +3 -7
  261. package/src/IDBService.ts +78 -17
  262. package/src/UndoHistory.ts +5 -3
  263. package/src/__tests__/batching.test.ts +5 -2
  264. package/src/__tests__/documents.test.ts +66 -6
  265. package/src/__tests__/fixtures/testStorage.ts +9 -0
  266. package/src/__tests__/legacyOids.test.ts +53 -17
  267. package/src/__tests__/mutations.test.ts +9 -3
  268. package/src/__tests__/queries.test.ts +6 -2
  269. package/src/__tests__/setup/indexedDB.ts +14 -1
  270. package/src/__tests__/undo.test.ts +17 -9
  271. package/src/client/Client.ts +8 -4
  272. package/src/client/ClientDescriptor.ts +24 -8
  273. package/src/context.ts +16 -1
  274. package/src/entities/2/Entity.test.ts +218 -0
  275. package/src/entities/2/Entity.ts +954 -0
  276. package/src/entities/2/EntityCache.ts +41 -0
  277. package/src/entities/2/EntityMetadata.ts +364 -0
  278. package/src/entities/2/EntityStore.ts +490 -0
  279. package/src/entities/2/NOTES.md +22 -0
  280. package/src/entities/2/OperationBatcher.ts +251 -0
  281. package/src/entities/2/types.ts +154 -0
  282. package/src/files/EntityFile.ts +9 -4
  283. package/src/files/FileManager.ts +5 -3
  284. package/src/files/FileStorage.ts +7 -13
  285. package/src/files/utils.ts +9 -1
  286. package/src/idb.ts +51 -3
  287. package/src/index.ts +2 -2
  288. package/src/metadata/AckInfoStore.ts +1 -1
  289. package/src/metadata/BaselinesStore.ts +16 -24
  290. package/src/metadata/LocalReplicaStore.ts +13 -6
  291. package/src/metadata/Metadata.ts +109 -24
  292. package/src/metadata/OperationsStore.ts +37 -16
  293. package/src/migration/openDatabase.ts +32 -10
  294. package/src/queries/BaseQuery.ts +15 -2
  295. package/src/queries/CollectionQueries.ts +3 -3
  296. package/src/queries/FindAllQuery.ts +4 -0
  297. package/src/queries/QueryCache.ts +5 -0
  298. package/src/queries/QueryableStorage.ts +107 -0
  299. package/src/queries/dbQueries.ts +10 -3
  300. package/src/queries/utils.ts +1 -1
  301. package/src/sync/FileSync.ts +2 -0
  302. package/src/sync/PushPullSync.ts +8 -1
  303. package/src/sync/Sync.ts +14 -6
  304. package/src/sync/WebSocketSync.ts +47 -10
  305. package/src/entities/DocumentFamiliyCache.ts +0 -426
  306. package/src/entities/Entity.ts +0 -874
  307. package/src/entities/EntityStore.ts +0 -731
@@ -0,0 +1,490 @@
1
+ import {
2
+ DocumentBaseline,
3
+ ObjectIdentifier,
4
+ Operation,
5
+ StorageFieldsSchema,
6
+ StorageObjectFieldSchema,
7
+ assert,
8
+ assignOid,
9
+ decomposeOid,
10
+ getOidRoot,
11
+ groupBaselinesByRootOid,
12
+ groupPatchesByOid,
13
+ groupPatchesByRootOid,
14
+ isRootOid,
15
+ removeOidsFromAllSubObjects,
16
+ } from '@verdant-web/common';
17
+ import { Context } from '../../context.js';
18
+ import { Metadata } from '../../metadata/Metadata.js';
19
+ import { Entity } from './Entity.js';
20
+ import { Disposable } from '../../utils/Disposable.js';
21
+ import { EntityFamilyMetadata } from './EntityMetadata.js';
22
+ import { FileManager } from '../../files/FileManager.js';
23
+ import { OperationBatcher } from './OperationBatcher.js';
24
+ import { QueryableStorage } from '../../queries/QueryableStorage.js';
25
+ import { WeakEvent } from 'weak-event';
26
+ import { processValueFiles } from '../../files/utils.js';
27
+ import { abort } from 'process';
28
+
29
+ enum AbortReason {
30
+ Reset,
31
+ }
32
+
33
+ export type EntityStoreEventData = {
34
+ oid: ObjectIdentifier;
35
+ operations?: Record<string, Operation[]>;
36
+ baselines?: DocumentBaseline[];
37
+ isLocal: boolean;
38
+ };
39
+
40
+ export type EntityStoreEvents = {
41
+ add: WeakEvent<EntityStore, EntityStoreEventData>;
42
+ replace: WeakEvent<EntityStore, EntityStoreEventData>;
43
+ resetAll: WeakEvent<EntityStore, void>;
44
+ };
45
+
46
+ type IncomingData = {
47
+ operations?: Operation[];
48
+ baselines?: DocumentBaseline[];
49
+ reset?: boolean;
50
+ isLocal?: boolean;
51
+ };
52
+
53
+ export class EntityStore extends Disposable {
54
+ private ctx;
55
+ private meta;
56
+ private files;
57
+ private batcher;
58
+ private queryableStorage;
59
+ private events: EntityStoreEvents = {
60
+ add: new WeakEvent(),
61
+ replace: new WeakEvent(),
62
+ resetAll: new WeakEvent(),
63
+ };
64
+ private cache = new Map<ObjectIdentifier, WeakRef<Entity>>();
65
+ private pendingEntityPromises = new Map<
66
+ ObjectIdentifier,
67
+ Promise<Entity | null>
68
+ >();
69
+ // halts the current data queue processing
70
+ private abortDataQueueController = new AbortController();
71
+ private ongoingResetPromise: Promise<void> | null = null;
72
+ private entityFinalizationRegistry = new FinalizationRegistry(
73
+ (oid: ObjectIdentifier) => {
74
+ this.ctx.log('debug', 'Entity GC', oid);
75
+ },
76
+ );
77
+
78
+ constructor({
79
+ ctx,
80
+ meta,
81
+ files,
82
+ }: {
83
+ ctx: Context;
84
+ meta: Metadata;
85
+ files: FileManager;
86
+ }) {
87
+ super();
88
+
89
+ this.ctx = ctx;
90
+ this.meta = meta;
91
+ this.files = files;
92
+ this.queryableStorage = new QueryableStorage({ ctx });
93
+ this.batcher = new OperationBatcher({
94
+ ctx,
95
+ meta,
96
+ entities: this,
97
+ });
98
+ }
99
+
100
+ // expose batch APIs
101
+ get batch() {
102
+ return this.batcher.batch;
103
+ }
104
+ get flushAllBatches() {
105
+ return this.batcher.flushAll;
106
+ }
107
+
108
+ // internal-ish API to load remote / stored data
109
+ addData = async (data: IncomingData) => {
110
+ if (this.disposed) {
111
+ this.ctx.log('warn', 'EntityStore is disposed, not adding incoming data');
112
+ return;
113
+ }
114
+ // for resets - abort any other changes, reset everything,
115
+ // then proceed
116
+ if (data.reset) {
117
+ this.ctx.log(
118
+ 'info',
119
+ 'Resetting local store to replicate remote synced data - dropping any current transactions',
120
+ );
121
+ // cancel any other ongoing data - it will all
122
+ // be replaced by the reset
123
+ this.abortDataQueueController.abort(AbortReason.Reset);
124
+ this.abortDataQueueController = new AbortController();
125
+ this.ongoingResetPromise = this.resetData().finally(() => {
126
+ this.ongoingResetPromise = null;
127
+ });
128
+ }
129
+
130
+ // await either the reset we just started, or any that was
131
+ // in progress when this data came in.
132
+ if (this.ongoingResetPromise) {
133
+ this.ctx.log('debug', 'Waiting for ongoing reset to complete');
134
+ await this.ongoingResetPromise;
135
+ this.ctx.log('debug', 'Ongoing reset complete');
136
+ }
137
+
138
+ await this.processData(data);
139
+ };
140
+
141
+ private resetData = async () => {
142
+ if (this.disposed) {
143
+ this.ctx.log('warn', 'EntityStore is disposed, not resetting local data');
144
+ return;
145
+ }
146
+ await this.meta.reset();
147
+ await this.queryableStorage.reset();
148
+ this.events.resetAll.invoke(this);
149
+ };
150
+
151
+ private processData = async (data: IncomingData) => {
152
+ if (this.disposed) {
153
+ this.ctx.log(
154
+ 'warn',
155
+ 'EntityStore is disposed, not processing incoming data',
156
+ );
157
+ return;
158
+ }
159
+
160
+ const baselines = data?.baselines ?? [];
161
+ const operations = data?.operations ?? [];
162
+
163
+ this.ctx.log('debug', 'Processing incoming data', {
164
+ operations: operations.length,
165
+ baselines: baselines.length,
166
+ reset: !!data.reset,
167
+ });
168
+
169
+ const allDocumentOids: ObjectIdentifier[] = Array.from(
170
+ new Set(
171
+ baselines
172
+ .map((b) => getOidRoot(b.oid))
173
+ .concat(operations.map((o) => getOidRoot(o.oid))),
174
+ ),
175
+ );
176
+ const baselinesGroupedByOid = groupBaselinesByRootOid(baselines);
177
+ const operationsGroupedByOid = groupPatchesByRootOid(operations);
178
+
179
+ this.ctx.log('debug', 'Applying data to live entities');
180
+ // synchronously add/replace data in any open entities via eventing
181
+ for (const oid of allDocumentOids) {
182
+ const baselines = baselinesGroupedByOid[oid];
183
+ const operations = operationsGroupedByOid[oid] ?? [];
184
+ const groupedOperations = groupPatchesByOid(operations);
185
+ // what happens if an entity is being hydrated
186
+ // while this is happening? - we wait for the hydration promise
187
+ // to complete, then invoke the event
188
+ const event = data.reset ? this.events.replace : this.events.add;
189
+ const hydrationPromise = this.pendingEntityPromises.get(oid);
190
+ if (hydrationPromise) {
191
+ hydrationPromise.then(() => {
192
+ event.invoke(this, {
193
+ oid,
194
+ baselines,
195
+ operations: groupedOperations,
196
+ isLocal: false,
197
+ });
198
+ });
199
+ } else {
200
+ if (this.cache.has(oid)) {
201
+ this.ctx.log('debug', 'Cache has', oid, ', an event should follow.');
202
+ }
203
+ event.invoke(this, {
204
+ oid,
205
+ baselines,
206
+ operations: groupedOperations,
207
+ isLocal: false,
208
+ });
209
+ }
210
+ }
211
+
212
+ const abortOptions = {
213
+ abort: this.abortDataQueueController.signal,
214
+ };
215
+
216
+ // then, asynchronously add to the database
217
+ await this.meta.insertData(data, abortOptions);
218
+
219
+ // FIXME: entities hydrated here are not seeing
220
+ // the operations just inserted above!!
221
+ // IDEA: can we coordinate here with hydrate promises
222
+ // based on affected OIDs?
223
+
224
+ // recompute all affected documents for querying
225
+ const entities = await Promise.all(
226
+ allDocumentOids.map(async (oid) => {
227
+ const entity = await this.hydrate(oid, abortOptions);
228
+ // if the entity is not found, we return a stub that
229
+ // indicates it's deleted and should be cleared
230
+ return (
231
+ entity ?? {
232
+ oid,
233
+ getSnapshot(): any {
234
+ return null;
235
+ },
236
+ }
237
+ );
238
+ }),
239
+ );
240
+ try {
241
+ await this.queryableStorage.saveEntities(entities, abortOptions);
242
+ } catch (err) {
243
+ if (this.disposed) {
244
+ this.ctx.log(
245
+ 'warn',
246
+ 'Error saving entities to queryable storage - EntityStore is disposed',
247
+ err,
248
+ );
249
+ } else {
250
+ this.ctx.log(
251
+ 'error',
252
+ 'Error saving entities to queryable storage',
253
+ err,
254
+ );
255
+ }
256
+ }
257
+ };
258
+
259
+ // internal-ish API for creating Entities from OIDs
260
+ // when query results come in
261
+ hydrate = async (
262
+ oid: string,
263
+ opts?: { abort: AbortSignal },
264
+ ): Promise<Entity | null> => {
265
+ if (!isRootOid(oid)) {
266
+ throw new Error('Cannot hydrate non-root entity');
267
+ }
268
+
269
+ if (this.cache.has(oid)) {
270
+ this.ctx.log('debug', 'Hydrating entity from cache', oid);
271
+ const cached = this.cache.get(oid);
272
+ if (cached) {
273
+ const entity = cached.deref();
274
+ if (entity) {
275
+ if (entity.deleted) {
276
+ return null;
277
+ }
278
+ return entity;
279
+ } else {
280
+ this.ctx.log('debug', "Removing GC'd entity from cache", oid);
281
+ this.cache.delete(oid);
282
+ }
283
+ }
284
+ }
285
+
286
+ // we don't want to hydrate two entities in parallel, so
287
+ // we use a promise to ensure that only one is ever
288
+ // constructed at a time
289
+ const pendingPromise = this.pendingEntityPromises.get(oid);
290
+ if (!pendingPromise) {
291
+ this.ctx.log('debug', 'Hydrating entity from storage', oid);
292
+ const entity = this.constructEntity(oid);
293
+ if (!entity) {
294
+ return null;
295
+ }
296
+ const pendingPromise = this.loadEntity(entity, opts);
297
+ pendingPromise.finally(() => {
298
+ this.pendingEntityPromises.delete(oid);
299
+ });
300
+ this.pendingEntityPromises.set(oid, pendingPromise);
301
+ return pendingPromise;
302
+ } else {
303
+ this.ctx.log('debug', 'Waiting for entity hydration', oid);
304
+ return pendingPromise;
305
+ }
306
+ };
307
+
308
+ destroy = async () => {
309
+ this.dispose();
310
+ await this.batcher.flushAll();
311
+ };
312
+
313
+ // public APIs for manipulating entities
314
+
315
+ /**
316
+ * Creates a new Entity with the given initial data.
317
+ */
318
+ create = async (initial: any, oid: ObjectIdentifier) => {
319
+ this.ctx.log('debug', 'Creating new entity', oid);
320
+ const { collection } = decomposeOid(oid);
321
+ // remove any OID associations from the initial data
322
+ removeOidsFromAllSubObjects(initial);
323
+ // grab files and replace them with refs
324
+ const processed = processValueFiles(initial, this.files.add);
325
+
326
+ assignOid(processed, oid);
327
+
328
+ // creating a new Entity with no data, then preloading the operations
329
+ const entity = this.constructEntity(oid);
330
+ if (!entity) {
331
+ throw new Error(
332
+ `Could not put new document: no schema exists for collection ${collection}`,
333
+ );
334
+ }
335
+
336
+ const operations = this.meta.patchCreator.createInitialize(processed, oid);
337
+ await this.batcher.commitOperations(operations, {
338
+ undoable: true,
339
+ source: entity,
340
+ });
341
+
342
+ // TODO: what happens if you create an entity with an OID that already
343
+ // exists?
344
+
345
+ // we still need to synchronously add the initial operations to the Entity
346
+ // even though they are flowing through the system
347
+ // TODO: this could be better aligned to avoid grouping here
348
+ const operationsGroupedByOid = groupPatchesByOid(operations);
349
+ this.events.add.invoke(this, {
350
+ operations: operationsGroupedByOid,
351
+ isLocal: true,
352
+ oid,
353
+ });
354
+ this.cache.set(oid, this.ctx.weakRef(entity));
355
+
356
+ return entity;
357
+ };
358
+
359
+ deleteAll = async (
360
+ oids: ObjectIdentifier[],
361
+ options?: { undoable?: boolean },
362
+ ) => {
363
+ this.ctx.log('info', 'Deleting documents', oids);
364
+ assert(
365
+ oids.every((oid) => oid === getOidRoot(oid)),
366
+ 'Only root documents may be deleted via client methods',
367
+ );
368
+
369
+ const allOids = await Promise.all(
370
+ oids.flatMap(async (oid) => {
371
+ const entity = await this.hydrate(oid);
372
+ return entity?.__getFamilyOids__() ?? [];
373
+ }),
374
+ );
375
+
376
+ // remove the entities from cache
377
+ oids.forEach((oid) => {
378
+ this.cache.delete(oid);
379
+ this.ctx.log('debug', 'Deleted document from cache', oid);
380
+ });
381
+
382
+ // create the delete patches and wait for them to be applied
383
+ const operations = this.meta.patchCreator.createDeleteAll(allOids.flat());
384
+ await this.batcher.commitOperations(operations, {
385
+ undoable: options?.undoable === undefined ? true : options.undoable,
386
+ });
387
+ };
388
+
389
+ delete = async (oid: ObjectIdentifier, options?: { undoable?: boolean }) => {
390
+ return this.deleteAll([oid], options);
391
+ };
392
+
393
+ private getCollectionSchema = (
394
+ collectionName: string,
395
+ ): {
396
+ schema: StorageObjectFieldSchema | null;
397
+ readonlyKeys: string[];
398
+ } => {
399
+ const schema = this.ctx.schema.collections[collectionName];
400
+ if (!schema) {
401
+ this.ctx.log('warn', `Missing schema for collection: ${collectionName}`);
402
+ return {
403
+ schema: null,
404
+ readonlyKeys: [],
405
+ };
406
+ }
407
+ return {
408
+ // convert to object schema for compatibility
409
+ schema: {
410
+ type: 'object',
411
+ nullable: false,
412
+ properties: schema.fields as any,
413
+ },
414
+ readonlyKeys: [schema.primaryKey],
415
+ };
416
+ };
417
+
418
+ /**
419
+ * Constructs an entity from an OID, but does not load it.
420
+ */
421
+ private constructEntity = (oid: string): Entity | null => {
422
+ const { collection } = decomposeOid(oid);
423
+ const { schema, readonlyKeys } = this.getCollectionSchema(collection);
424
+
425
+ if (!schema) {
426
+ return null;
427
+ }
428
+
429
+ if (this.disposed) {
430
+ throw new Error('Cannot hydrate entity after store has been disposed');
431
+ }
432
+
433
+ const metadataFamily = new EntityFamilyMetadata({
434
+ ctx: this.ctx,
435
+ onPendingOperations: this.onPendingOperations,
436
+ rootOid: oid,
437
+ });
438
+
439
+ // this is created synchronously so it's immediately available
440
+ // to begin capturing incoming data.
441
+ return new Entity({
442
+ ctx: this.ctx,
443
+ oid,
444
+ schema,
445
+ readonlyKeys,
446
+ files: this.files,
447
+ metadataFamily: metadataFamily,
448
+ patchCreator: this.meta.patchCreator,
449
+ events: this.events,
450
+ });
451
+ };
452
+
453
+ private onPendingOperations = (operations: Operation[]) => {
454
+ this.batcher.addOperations(operations);
455
+ };
456
+
457
+ /**
458
+ * Loads initial Entity data from storage
459
+ */
460
+ private loadEntity = async (
461
+ entity: Entity,
462
+ opts?: { abort: AbortSignal },
463
+ ): Promise<Entity | null> => {
464
+ const { operations, baselines } = await this.meta.getDocumentData(
465
+ entity.oid,
466
+ opts,
467
+ );
468
+
469
+ if (!baselines.length && !Object.keys(operations).length) {
470
+ this.ctx.log('debug', 'No data found for entity', entity.oid);
471
+ return null;
472
+ }
473
+
474
+ this.ctx.log('debug', 'Loaded entity from storage', entity.oid);
475
+
476
+ this.events.replace.invoke(this, {
477
+ oid: entity.oid,
478
+ baselines,
479
+ operations,
480
+ isLocal: false,
481
+ });
482
+
483
+ // only set the cache after loading.
484
+ // TODO: is this cache/promise stuff redundant?
485
+ this.cache.set(entity.oid, this.ctx.weakRef(entity));
486
+ this.entityFinalizationRegistry.register(entity, entity.oid);
487
+
488
+ return entity;
489
+ };
490
+ }
@@ -0,0 +1,22 @@
1
+ Starting with a snapshot, a root Entity is created
2
+
3
+ The root snapshot is maintained and updated as things change
4
+
5
+ All child entities keep a reference to their portion of the root snapshot
6
+
7
+ Child entities are cached in their parent
8
+ A child is only ever a child of one parent - no reparenting
9
+
10
+ On change, an event is forwarded to the appropriate entity in the tree
11
+ how?
12
+
13
+ Migration stuff...
14
+
15
+ During migration, create a 'touched' empty op for every single root OID.
16
+
17
+ When loading a document, if there isn't an op for this version, we know the doc is not migrated. The range for migration is from the version of its current op/baseline.
18
+
19
+ # Lifecycle of an Entity
20
+
21
+ At first, we've come from the queryable storage, so we have a plain object
22
+ representing the entity's snapshot. But without associated OIDs, we can't do much besides read the data. For now, this initial snapshot isn't used.