@verdant-web/store 2.8.5 → 3.0.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 (278) 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 +1 -1
  4. package/dist/cjs/DocumentManager.js +1 -1
  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 +37 -6
  15. package/dist/cjs/__tests__/documents.test.js.map +1 -1
  16. package/dist/cjs/__tests__/fixtures/testStorage.d.ts +2 -2
  17. package/dist/cjs/__tests__/fixtures/testStorage.js +2 -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 +8 -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 +1 -1
  31. package/dist/cjs/client/Client.js +7 -3
  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/Entity.d.ts +106 -178
  37. package/dist/cjs/entities/Entity.js +558 -376
  38. package/dist/cjs/entities/Entity.js.map +1 -1
  39. package/dist/cjs/entities/Entity.test.d.ts +1 -0
  40. package/dist/cjs/entities/Entity.test.js +194 -0
  41. package/dist/cjs/entities/Entity.test.js.map +1 -0
  42. package/dist/cjs/entities/EntityCache.d.ts +15 -0
  43. package/dist/cjs/entities/EntityCache.js +39 -0
  44. package/dist/cjs/entities/EntityCache.js.map +1 -0
  45. package/dist/cjs/entities/EntityMetadata.d.ts +68 -0
  46. package/dist/cjs/entities/EntityMetadata.js +261 -0
  47. package/dist/cjs/entities/EntityMetadata.js.map +1 -0
  48. package/dist/cjs/entities/EntityStore.d.ts +63 -68
  49. package/dist/cjs/entities/EntityStore.js +294 -438
  50. package/dist/cjs/entities/EntityStore.js.map +1 -1
  51. package/dist/cjs/entities/OperationBatcher.d.ts +52 -0
  52. package/dist/cjs/entities/OperationBatcher.js +165 -0
  53. package/dist/cjs/entities/OperationBatcher.js.map +1 -0
  54. package/dist/cjs/entities/types.d.ts +84 -0
  55. package/dist/cjs/entities/types.js +3 -0
  56. package/dist/cjs/entities/types.js.map +1 -0
  57. package/dist/cjs/files/EntityFile.d.ts +5 -2
  58. package/dist/cjs/files/EntityFile.js +8 -4
  59. package/dist/cjs/files/EntityFile.js.map +1 -1
  60. package/dist/cjs/files/FileManager.d.ts +3 -1
  61. package/dist/cjs/files/FileManager.js +5 -3
  62. package/dist/cjs/files/FileManager.js.map +1 -1
  63. package/dist/cjs/files/FileStorage.js +7 -7
  64. package/dist/cjs/files/FileStorage.js.map +1 -1
  65. package/dist/cjs/files/utils.d.ts +2 -0
  66. package/dist/cjs/files/utils.js +5 -2
  67. package/dist/cjs/files/utils.js.map +1 -1
  68. package/dist/cjs/idb.d.ts +2 -0
  69. package/dist/cjs/idb.js +50 -4
  70. package/dist/cjs/idb.js.map +1 -1
  71. package/dist/cjs/index.d.ts +1 -1
  72. package/dist/cjs/metadata/AckInfoStore.js +1 -1
  73. package/dist/cjs/metadata/AckInfoStore.js.map +1 -1
  74. package/dist/cjs/metadata/BaselinesStore.d.ts +4 -1
  75. package/dist/cjs/metadata/BaselinesStore.js +19 -10
  76. package/dist/cjs/metadata/BaselinesStore.js.map +1 -1
  77. package/dist/cjs/metadata/LocalReplicaStore.d.ts +1 -1
  78. package/dist/cjs/metadata/LocalReplicaStore.js +11 -5
  79. package/dist/cjs/metadata/LocalReplicaStore.js.map +1 -1
  80. package/dist/cjs/metadata/Metadata.d.ts +26 -5
  81. package/dist/cjs/metadata/Metadata.js +55 -18
  82. package/dist/cjs/metadata/Metadata.js.map +1 -1
  83. package/dist/cjs/metadata/OperationsStore.d.ts +3 -0
  84. package/dist/cjs/metadata/OperationsStore.js +35 -15
  85. package/dist/cjs/metadata/OperationsStore.js.map +1 -1
  86. package/dist/cjs/migration/openDatabase.js +31 -10
  87. package/dist/cjs/migration/openDatabase.js.map +1 -1
  88. package/dist/cjs/queries/BaseQuery.js +13 -1
  89. package/dist/cjs/queries/BaseQuery.js.map +1 -1
  90. package/dist/cjs/queries/CollectionQueries.js +1 -1
  91. package/dist/cjs/queries/CollectionQueries.js.map +1 -1
  92. package/dist/cjs/queries/FindAllQuery.js +1 -0
  93. package/dist/cjs/queries/FindAllQuery.js.map +1 -1
  94. package/dist/cjs/queries/QueryCache.d.ts +1 -0
  95. package/dist/cjs/queries/QueryCache.js +4 -0
  96. package/dist/cjs/queries/QueryCache.js.map +1 -1
  97. package/dist/cjs/queries/QueryableStorage.d.ts +20 -0
  98. package/dist/cjs/queries/QueryableStorage.js +84 -0
  99. package/dist/cjs/queries/QueryableStorage.js.map +1 -0
  100. package/dist/cjs/queries/dbQueries.js +13 -3
  101. package/dist/cjs/queries/dbQueries.js.map +1 -1
  102. package/dist/cjs/sync/FileSync.d.ts +1 -0
  103. package/dist/cjs/sync/FileSync.js +1 -0
  104. package/dist/cjs/sync/FileSync.js.map +1 -1
  105. package/dist/cjs/sync/PushPullSync.d.ts +2 -1
  106. package/dist/cjs/sync/PushPullSync.js +7 -1
  107. package/dist/cjs/sync/PushPullSync.js.map +1 -1
  108. package/dist/cjs/sync/Sync.d.ts +6 -3
  109. package/dist/cjs/sync/Sync.js +9 -4
  110. package/dist/cjs/sync/Sync.js.map +1 -1
  111. package/dist/cjs/sync/WebSocketSync.d.ts +4 -1
  112. package/dist/cjs/sync/WebSocketSync.js +41 -11
  113. package/dist/cjs/sync/WebSocketSync.js.map +1 -1
  114. package/dist/esm/DocumentManager.d.ts +1 -1
  115. package/dist/esm/DocumentManager.js +1 -1
  116. package/dist/esm/DocumentManager.js.map +1 -1
  117. package/dist/esm/IDBService.d.ts +28 -7
  118. package/dist/esm/IDBService.js +51 -14
  119. package/dist/esm/IDBService.js.map +1 -1
  120. package/dist/esm/UndoHistory.d.ts +1 -1
  121. package/dist/esm/UndoHistory.js +6 -2
  122. package/dist/esm/UndoHistory.js.map +1 -1
  123. package/dist/esm/__tests__/batching.test.js +3 -1
  124. package/dist/esm/__tests__/batching.test.js.map +1 -1
  125. package/dist/esm/__tests__/documents.test.js +37 -6
  126. package/dist/esm/__tests__/documents.test.js.map +1 -1
  127. package/dist/esm/__tests__/fixtures/testStorage.d.ts +2 -2
  128. package/dist/esm/__tests__/fixtures/testStorage.js +2 -1
  129. package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
  130. package/dist/esm/__tests__/legacyOids.test.js +50 -17
  131. package/dist/esm/__tests__/legacyOids.test.js.map +1 -1
  132. package/dist/esm/__tests__/mutations.test.js +9 -3
  133. package/dist/esm/__tests__/mutations.test.js.map +1 -1
  134. package/dist/esm/__tests__/queries.test.js +6 -2
  135. package/dist/esm/__tests__/queries.test.js.map +1 -1
  136. package/dist/esm/__tests__/setup/indexedDB.d.ts +1 -1
  137. package/dist/esm/__tests__/setup/indexedDB.js +8 -1
  138. package/dist/esm/__tests__/setup/indexedDB.js.map +1 -1
  139. package/dist/esm/__tests__/undo.test.js +16 -9
  140. package/dist/esm/__tests__/undo.test.js.map +1 -1
  141. package/dist/esm/client/Client.d.ts +1 -1
  142. package/dist/esm/client/Client.js +7 -3
  143. package/dist/esm/client/Client.js.map +1 -1
  144. package/dist/esm/client/ClientDescriptor.js +21 -6
  145. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  146. package/dist/esm/context.d.ts +10 -1
  147. package/dist/esm/entities/Entity.d.ts +106 -178
  148. package/dist/esm/entities/Entity.js +559 -376
  149. package/dist/esm/entities/Entity.js.map +1 -1
  150. package/dist/esm/entities/Entity.test.d.ts +1 -0
  151. package/dist/esm/entities/Entity.test.js +192 -0
  152. package/dist/esm/entities/Entity.test.js.map +1 -0
  153. package/dist/esm/entities/EntityCache.d.ts +15 -0
  154. package/dist/esm/entities/EntityCache.js +35 -0
  155. package/dist/esm/entities/EntityCache.js.map +1 -0
  156. package/dist/esm/entities/EntityMetadata.d.ts +68 -0
  157. package/dist/esm/entities/EntityMetadata.js +256 -0
  158. package/dist/esm/entities/EntityMetadata.js.map +1 -0
  159. package/dist/esm/entities/EntityStore.d.ts +63 -68
  160. package/dist/esm/entities/EntityStore.js +295 -439
  161. package/dist/esm/entities/EntityStore.js.map +1 -1
  162. package/dist/esm/entities/OperationBatcher.d.ts +52 -0
  163. package/dist/esm/entities/OperationBatcher.js +161 -0
  164. package/dist/esm/entities/OperationBatcher.js.map +1 -0
  165. package/dist/esm/entities/types.d.ts +84 -0
  166. package/dist/esm/entities/types.js +2 -0
  167. package/dist/esm/entities/types.js.map +1 -0
  168. package/dist/esm/files/EntityFile.d.ts +5 -2
  169. package/dist/esm/files/EntityFile.js +8 -4
  170. package/dist/esm/files/EntityFile.js.map +1 -1
  171. package/dist/esm/files/FileManager.d.ts +3 -1
  172. package/dist/esm/files/FileManager.js +5 -3
  173. package/dist/esm/files/FileManager.js.map +1 -1
  174. package/dist/esm/files/FileStorage.js +7 -7
  175. package/dist/esm/files/FileStorage.js.map +1 -1
  176. package/dist/esm/files/utils.d.ts +2 -0
  177. package/dist/esm/files/utils.js +4 -2
  178. package/dist/esm/files/utils.js.map +1 -1
  179. package/dist/esm/idb.d.ts +2 -0
  180. package/dist/esm/idb.js +47 -3
  181. package/dist/esm/idb.js.map +1 -1
  182. package/dist/esm/index.d.ts +1 -1
  183. package/dist/esm/metadata/AckInfoStore.js +1 -1
  184. package/dist/esm/metadata/AckInfoStore.js.map +1 -1
  185. package/dist/esm/metadata/BaselinesStore.d.ts +4 -1
  186. package/dist/esm/metadata/BaselinesStore.js +19 -10
  187. package/dist/esm/metadata/BaselinesStore.js.map +1 -1
  188. package/dist/esm/metadata/LocalReplicaStore.d.ts +1 -1
  189. package/dist/esm/metadata/LocalReplicaStore.js +11 -5
  190. package/dist/esm/metadata/LocalReplicaStore.js.map +1 -1
  191. package/dist/esm/metadata/Metadata.d.ts +26 -5
  192. package/dist/esm/metadata/Metadata.js +56 -19
  193. package/dist/esm/metadata/Metadata.js.map +1 -1
  194. package/dist/esm/metadata/OperationsStore.d.ts +3 -0
  195. package/dist/esm/metadata/OperationsStore.js +35 -15
  196. package/dist/esm/metadata/OperationsStore.js.map +1 -1
  197. package/dist/esm/migration/openDatabase.js +32 -11
  198. package/dist/esm/migration/openDatabase.js.map +1 -1
  199. package/dist/esm/queries/BaseQuery.js +13 -1
  200. package/dist/esm/queries/BaseQuery.js.map +1 -1
  201. package/dist/esm/queries/CollectionQueries.js +1 -1
  202. package/dist/esm/queries/CollectionQueries.js.map +1 -1
  203. package/dist/esm/queries/FindAllQuery.js +1 -0
  204. package/dist/esm/queries/FindAllQuery.js.map +1 -1
  205. package/dist/esm/queries/QueryCache.d.ts +1 -0
  206. package/dist/esm/queries/QueryCache.js +4 -0
  207. package/dist/esm/queries/QueryCache.js.map +1 -1
  208. package/dist/esm/queries/QueryableStorage.d.ts +20 -0
  209. package/dist/esm/queries/QueryableStorage.js +80 -0
  210. package/dist/esm/queries/QueryableStorage.js.map +1 -0
  211. package/dist/esm/queries/dbQueries.js +13 -3
  212. package/dist/esm/queries/dbQueries.js.map +1 -1
  213. package/dist/esm/sync/FileSync.d.ts +1 -0
  214. package/dist/esm/sync/FileSync.js +1 -0
  215. package/dist/esm/sync/FileSync.js.map +1 -1
  216. package/dist/esm/sync/PushPullSync.d.ts +2 -1
  217. package/dist/esm/sync/PushPullSync.js +7 -1
  218. package/dist/esm/sync/PushPullSync.js.map +1 -1
  219. package/dist/esm/sync/Sync.d.ts +6 -3
  220. package/dist/esm/sync/Sync.js +9 -4
  221. package/dist/esm/sync/Sync.js.map +1 -1
  222. package/dist/esm/sync/WebSocketSync.d.ts +4 -1
  223. package/dist/esm/sync/WebSocketSync.js +41 -11
  224. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  225. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  226. package/dist/tsconfig.tsbuildinfo +1 -1
  227. package/package.json +8 -7
  228. package/src/DocumentManager.ts +1 -1
  229. package/src/IDBService.ts +78 -17
  230. package/src/UndoHistory.ts +5 -3
  231. package/src/__tests__/batching.test.ts +5 -2
  232. package/src/__tests__/documents.test.ts +44 -6
  233. package/src/__tests__/fixtures/testStorage.ts +3 -0
  234. package/src/__tests__/legacyOids.test.ts +53 -17
  235. package/src/__tests__/mutations.test.ts +9 -3
  236. package/src/__tests__/queries.test.ts +6 -2
  237. package/src/__tests__/setup/indexedDB.ts +8 -1
  238. package/src/__tests__/undo.test.ts +17 -9
  239. package/src/client/Client.ts +7 -3
  240. package/src/client/ClientDescriptor.ts +24 -8
  241. package/src/context.ts +16 -1
  242. package/src/entities/Entity.test.ts +218 -0
  243. package/src/entities/Entity.ts +696 -616
  244. package/src/entities/EntityCache.ts +41 -0
  245. package/src/entities/EntityMetadata.ts +364 -0
  246. package/src/entities/EntityStore.ts +384 -621
  247. package/src/entities/OperationBatcher.ts +251 -0
  248. package/src/entities/types.ts +154 -0
  249. package/src/files/EntityFile.ts +9 -4
  250. package/src/files/FileManager.ts +5 -3
  251. package/src/files/FileStorage.ts +7 -13
  252. package/src/files/utils.ts +6 -2
  253. package/src/idb.ts +51 -3
  254. package/src/index.ts +1 -1
  255. package/src/metadata/AckInfoStore.ts +1 -1
  256. package/src/metadata/BaselinesStore.ts +16 -24
  257. package/src/metadata/LocalReplicaStore.ts +13 -6
  258. package/src/metadata/Metadata.ts +109 -24
  259. package/src/metadata/OperationsStore.ts +37 -16
  260. package/src/migration/openDatabase.ts +32 -10
  261. package/src/queries/BaseQuery.ts +14 -1
  262. package/src/queries/CollectionQueries.ts +1 -1
  263. package/src/queries/FindAllQuery.ts +4 -0
  264. package/src/queries/QueryCache.ts +5 -0
  265. package/src/queries/QueryableStorage.ts +107 -0
  266. package/src/queries/dbQueries.ts +10 -3
  267. package/src/sync/FileSync.ts +2 -0
  268. package/src/sync/PushPullSync.ts +8 -1
  269. package/src/sync/Sync.ts +14 -6
  270. package/src/sync/WebSocketSync.ts +47 -10
  271. package/dist/cjs/entities/DocumentFamiliyCache.d.ts +0 -96
  272. package/dist/cjs/entities/DocumentFamiliyCache.js +0 -287
  273. package/dist/cjs/entities/DocumentFamiliyCache.js.map +0 -1
  274. package/dist/esm/entities/DocumentFamiliyCache.d.ts +0 -96
  275. package/dist/esm/entities/DocumentFamiliyCache.js +0 -283
  276. package/dist/esm/entities/DocumentFamiliyCache.js.map +0 -1
  277. package/src/entities/DocumentFamiliyCache.ts +0 -426
  278. package/src/entities/design.tldr +0 -808
@@ -0,0 +1,41 @@
1
+ import { Entity, EntityInit } from './Entity.js';
2
+ import { EntityFile } from '../files/EntityFile.js';
3
+ import { ObjectIdentifier } from '@verdant-web/common';
4
+
5
+ export class EntityCache {
6
+ private cache = new Map<string, Entity | EntityFile>();
7
+
8
+ constructor({ initial }: { initial?: Entity[] } = {}) {
9
+ if (initial) {
10
+ for (const entity of initial) {
11
+ this.cache.set(entity.oid, entity);
12
+ }
13
+ }
14
+ }
15
+
16
+ get = (init: EntityInit): Entity => {
17
+ if (this.cache.has(init.oid)) {
18
+ return this.cache.get(init.oid)! as Entity;
19
+ }
20
+ const entity = new Entity(init);
21
+ this.cache.set(init.oid, entity);
22
+ return entity;
23
+ };
24
+
25
+ has = (oid: ObjectIdentifier) => {
26
+ return this.cache.has(oid);
27
+ };
28
+
29
+ getFile = (id: string, options: { downloadRemote: boolean }): EntityFile => {
30
+ if (this.cache.has(id)) {
31
+ return this.cache.get(id)! as EntityFile;
32
+ }
33
+ const file = new EntityFile(id, options);
34
+ this.cache.set(id, file);
35
+ return file;
36
+ };
37
+
38
+ getCached = (oid: string) => {
39
+ return this.cache.get(oid);
40
+ };
41
+ }
@@ -0,0 +1,364 @@
1
+ import {
2
+ DocumentBaseline,
3
+ ObjectIdentifier,
4
+ Operation,
5
+ applyPatch,
6
+ areOidsRelated,
7
+ assert,
8
+ assignOid,
9
+ cloneDeep,
10
+ compareTimestampSchemaVersions,
11
+ getWallClockTime,
12
+ } from '@verdant-web/common';
13
+ import { Context } from '../context.js';
14
+ import { EntityChange } from './types.js';
15
+
16
+ export type EntityMetadataView = {
17
+ view: any;
18
+ fromOlderVersion: boolean;
19
+ deleted: boolean;
20
+ empty: boolean;
21
+ updatedAt: number;
22
+ };
23
+
24
+ export class EntityMetadata {
25
+ private ctx;
26
+ private baseline: DocumentBaseline | null = null;
27
+ // these must be kept in timestamp order.
28
+ private confirmedOperations: Operation[] = [];
29
+ private pendingOperations: Operation[] = [];
30
+ readonly oid;
31
+
32
+ constructor({
33
+ oid,
34
+ ctx,
35
+ confirmedOperations,
36
+ pendingOperations,
37
+ baseline,
38
+ }: {
39
+ oid: ObjectIdentifier;
40
+ ctx: Context;
41
+ confirmedOperations?: Operation[];
42
+ pendingOperations?: Operation[];
43
+ baseline?: DocumentBaseline;
44
+ }) {
45
+ assert(oid, 'oid is required');
46
+ this.ctx = ctx;
47
+ this.oid = oid;
48
+ if (confirmedOperations) {
49
+ this.confirmedOperations = confirmedOperations;
50
+ }
51
+ if (pendingOperations) {
52
+ this.pendingOperations = pendingOperations;
53
+ }
54
+ if (baseline) {
55
+ this.baseline = baseline;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Compute the current view of the entity.
61
+ */
62
+ computeView = (omitPending = false): EntityMetadataView => {
63
+ const base = cloneDeep(this.baseline?.snapshot ?? undefined);
64
+ const baselineTimestamp = this.baseline?.timestamp ?? null;
65
+ const confirmedResult = this.applyOperations(
66
+ // apply ops to baseline
67
+ base,
68
+ // deleted if there's no baseline
69
+ !base,
70
+ // we're applying confirmed ops first
71
+ this.confirmedOperations,
72
+ // latest timestamp is the baseline timestamp, if any
73
+ baselineTimestamp,
74
+ // only apply ops after the baseline timestamp
75
+ baselineTimestamp,
76
+ );
77
+ // now's the time to declare we saw the future if we did.
78
+ if (confirmedResult.futureSeen) {
79
+ this.ctx.globalEvents.emit('futureSeen', confirmedResult.futureSeen);
80
+ }
81
+ const pendingResult = omitPending
82
+ ? confirmedResult
83
+ : this.applyOperations(
84
+ confirmedResult.view,
85
+ confirmedResult.deleted,
86
+ // now we're applying pending operations
87
+ this.pendingOperations,
88
+ // keep our latest timestamp up to date
89
+ confirmedResult.latestTimestamp,
90
+ // we don't use after for pending ops, they're all
91
+ // logically in the future
92
+ null,
93
+ );
94
+ // before letting this data out into the wild, we need
95
+ // to associate its oid
96
+ if (pendingResult.view) {
97
+ assignOid(pendingResult.view, this.oid);
98
+ }
99
+
100
+ // note whether confirmed data has an operation/baseline from the current
101
+ // schema or not.
102
+ const fromOlderVersion =
103
+ !!confirmedResult.latestTimestamp &&
104
+ compareTimestampSchemaVersions(
105
+ confirmedResult.latestTimestamp,
106
+ this.ctx.getNow(),
107
+ ) < 0;
108
+
109
+ const empty =
110
+ !this.baseline &&
111
+ !this.pendingOperations.length &&
112
+ !this.confirmedOperations.length;
113
+ if (empty) {
114
+ this.ctx.log('warn', `Tried to load Entity ${this.oid} with no data`);
115
+ }
116
+
117
+ const updatedAtTimestamp =
118
+ pendingResult.latestTimestamp ??
119
+ confirmedResult.latestTimestamp ??
120
+ baselineTimestamp;
121
+ const updatedAt = updatedAtTimestamp
122
+ ? getWallClockTime(updatedAtTimestamp)
123
+ : 0;
124
+
125
+ if (!pendingResult.view && !pendingResult.deleted && !empty) {
126
+ this.ctx.log(
127
+ 'warn',
128
+ `Entity ${this.oid} has no view, no deleted flag, and not empty`,
129
+ );
130
+ debugger;
131
+ }
132
+
133
+ return {
134
+ view: pendingResult.view ?? undefined,
135
+ deleted: pendingResult.deleted,
136
+ empty,
137
+ fromOlderVersion,
138
+ updatedAt,
139
+ };
140
+ };
141
+
142
+ addBaseline = (baseline: DocumentBaseline): void => {
143
+ // opt out if our baseline is newer
144
+ if (this.baseline && this.baseline.timestamp >= baseline.timestamp) {
145
+ return;
146
+ }
147
+ this.baseline = baseline;
148
+ // we can now drop any confirmed ops older than the baseline
149
+ while (this.confirmedOperations[0]?.timestamp < baseline.timestamp) {
150
+ this.confirmedOperations.shift();
151
+ }
152
+ };
153
+
154
+ /**
155
+ * @returns total number of new operations added
156
+ */
157
+ addConfirmedOperations = (operations: Operation[]): number => {
158
+ let totalAdded = 0;
159
+ // the operations must be inserted in timestamp order
160
+ for (const op of operations) {
161
+ const index = this.confirmedOperations.findIndex(
162
+ (o) => o.timestamp >= op.timestamp,
163
+ );
164
+ if (index !== -1) {
165
+ // ensure we don't have a duplicate
166
+ if (this.confirmedOperations[index].timestamp !== op.timestamp) {
167
+ // otherwise, insert at the right place
168
+ this.confirmedOperations.splice(index, 0, op);
169
+ totalAdded++;
170
+ }
171
+ } else {
172
+ // otherwise, append
173
+ this.confirmedOperations.push(op);
174
+ totalAdded++;
175
+ }
176
+ // FIXME: seems inefficient
177
+ // remove this incoming op from pending if it's in there
178
+ const pendingPrior = this.pendingOperations.length;
179
+ this.pendingOperations = this.pendingOperations.filter(
180
+ (pendingOp) => op.timestamp !== pendingOp.timestamp,
181
+ );
182
+ totalAdded -= pendingPrior - this.pendingOperations.length;
183
+ }
184
+ return totalAdded;
185
+ };
186
+
187
+ addPendingOperation = (operation: Operation) => {
188
+ // we can assume pending ops are always newer
189
+ this.pendingOperations.push(operation);
190
+ };
191
+
192
+ private applyOperations = (
193
+ base: any,
194
+ deleted: boolean,
195
+ operations: Operation[],
196
+ latestTimestamp: string | null,
197
+ after: string | null,
198
+ ): {
199
+ view: any;
200
+ latestTimestamp: string | null;
201
+ deleted: boolean;
202
+ futureSeen: string | undefined;
203
+ } => {
204
+ let futureSeen: string | undefined = undefined;
205
+ const now = this.ctx.getNow();
206
+ for (const op of operations) {
207
+ // ignore ops before our after cutoff
208
+ if (after && op.timestamp <= after) {
209
+ continue;
210
+ }
211
+ // don't apply future ops
212
+ if (compareTimestampSchemaVersions(op.timestamp, now) > 0) {
213
+ futureSeen = op.timestamp;
214
+ continue;
215
+ }
216
+ // we don't actually delete the view when a delete op
217
+ // comes in. the view remains useful for calculating
218
+ // undo operations.
219
+ if (op.data.op === 'delete') {
220
+ deleted = true;
221
+ } else {
222
+ base = applyPatch(base, op.data);
223
+ if (op.data.op === 'initialize') {
224
+ deleted = false;
225
+ }
226
+ }
227
+
228
+ // track the latest timestamp
229
+ if (!latestTimestamp || op.timestamp > latestTimestamp) {
230
+ latestTimestamp = op.timestamp;
231
+ }
232
+ }
233
+ return {
234
+ view: base,
235
+ latestTimestamp: latestTimestamp ?? null,
236
+ deleted,
237
+ futureSeen,
238
+ };
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Represents the metadata for a group of entities underneath a Document.
244
+ * Metadata is separated out this way so that these classes can be
245
+ * garbage collected when the root Document goes out of scope.
246
+ */
247
+ export class EntityFamilyMetadata {
248
+ private ctx;
249
+ private entities: Map<ObjectIdentifier, EntityMetadata> = new Map();
250
+ private onPendingOperations;
251
+ private rootOid: ObjectIdentifier;
252
+
253
+ constructor({
254
+ ctx,
255
+ onPendingOperations,
256
+ rootOid,
257
+ }: {
258
+ ctx: Context;
259
+ onPendingOperations: (ops: Operation[]) => void;
260
+ rootOid: ObjectIdentifier;
261
+ }) {
262
+ this.ctx = ctx;
263
+ this.rootOid = rootOid;
264
+ this.onPendingOperations = onPendingOperations;
265
+ }
266
+
267
+ get = (oid: ObjectIdentifier) => {
268
+ assert(oid, 'oid is required');
269
+ if (!this.entities.has(oid)) {
270
+ this.entities.set(oid, new EntityMetadata({ oid, ctx: this.ctx }));
271
+ }
272
+ return this.entities.get(oid)!;
273
+ };
274
+
275
+ getAllOids = () => {
276
+ return Array.from(this.entities.keys());
277
+ };
278
+
279
+ addConfirmedData = ({
280
+ baselines = [],
281
+ operations = {},
282
+ isLocal = false,
283
+ }: {
284
+ baselines?: DocumentBaseline[];
285
+ operations?: Record<ObjectIdentifier, Operation[]>;
286
+ isLocal?: boolean;
287
+ }) => {
288
+ const changes: Record<ObjectIdentifier, EntityChange> = {};
289
+ for (const baseline of baselines) {
290
+ if (!areOidsRelated(this.rootOid, baseline.oid)) {
291
+ throw new Error(
292
+ `Invalid baseline for entity ${this.rootOid}: ` +
293
+ JSON.stringify(baseline),
294
+ );
295
+ }
296
+ this.get(baseline.oid).addBaseline(baseline);
297
+ }
298
+ for (const [oid, ops] of Object.entries(operations)) {
299
+ if (!areOidsRelated(this.rootOid, oid)) {
300
+ throw new Error(
301
+ `Invalid operations for entity ${this.rootOid}: ` +
302
+ JSON.stringify(ops),
303
+ );
304
+ }
305
+ const added = this.get(oid).addConfirmedOperations(ops);
306
+ if (added !== 0) {
307
+ changes[oid] ??= { oid, isLocal };
308
+ }
309
+ }
310
+ return Object.values(changes);
311
+ };
312
+
313
+ /**
314
+ * Adds local, unconfirmed operations to the system.
315
+ * The API is different here to streamline for the way
316
+ * local changes are usually handled, as a list.
317
+ */
318
+ addPendingData = (operations: Operation[]) => {
319
+ const changes: Record<ObjectIdentifier, EntityChange> = {};
320
+ for (const op of operations) {
321
+ this.get(op.oid).addPendingOperation(op);
322
+ changes[op.oid] ??= { oid: op.oid, isLocal: true };
323
+ }
324
+ this.onPendingOperations(operations);
325
+ return Object.values(changes);
326
+ };
327
+
328
+ replaceAllData = ({
329
+ operations = {},
330
+ baselines = [],
331
+ }: {
332
+ operations?: Record<ObjectIdentifier, Operation[]>;
333
+ baselines?: DocumentBaseline[];
334
+ }) => {
335
+ const oids = Array.from(this.entities.keys());
336
+ this.entities.clear();
337
+ const changes: Record<ObjectIdentifier, EntityChange> = {};
338
+ // changes apply to all the entities we removed things from, too
339
+ for (const oid of oids) {
340
+ changes[oid] = { oid, isLocal: false };
341
+ }
342
+ for (const baseline of baselines) {
343
+ if (!areOidsRelated(this.rootOid, baseline.oid)) {
344
+ throw new Error(
345
+ `Invalid baseline for entity ${this.rootOid}: ` +
346
+ JSON.stringify(baseline),
347
+ );
348
+ }
349
+ this.get(baseline.oid).addBaseline(baseline);
350
+ changes[baseline.oid] ??= { oid: baseline.oid, isLocal: false };
351
+ }
352
+ for (const [oid, ops] of Object.entries(operations)) {
353
+ if (!areOidsRelated(this.rootOid, oid)) {
354
+ throw new Error(
355
+ `Invalid operations for entity ${this.rootOid}: ` +
356
+ JSON.stringify(ops),
357
+ );
358
+ }
359
+ this.get(oid).addConfirmedOperations(ops);
360
+ changes[oid] ??= { oid, isLocal: false };
361
+ }
362
+ return Object.values(changes);
363
+ };
364
+ }