@verdant-web/store 2.8.5 → 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 +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 +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 +5 -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 +37 -6
  142. package/dist/esm/__tests__/documents.test.js.map +1 -1
  143. package/dist/esm/__tests__/fixtures/testStorage.d.ts +2 -2
  144. package/dist/esm/__tests__/fixtures/testStorage.js +2 -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 +8 -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 +4 -2
  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 +44 -6
  265. package/src/__tests__/fixtures/testStorage.ts +3 -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 +8 -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 +6 -2
  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,954 @@
1
+ import {
2
+ DocumentBaseline,
3
+ EntityValidationProblem,
4
+ EventSubscriber,
5
+ ObjectIdentifier,
6
+ Operation,
7
+ PatchCreator,
8
+ StorageFieldSchema,
9
+ StorageFieldsSchema,
10
+ assert,
11
+ assignOid,
12
+ cloneDeep,
13
+ compareRefs,
14
+ createFileRef,
15
+ createRef,
16
+ getChildFieldSchema,
17
+ getDefault,
18
+ hasDefault,
19
+ isFileRef,
20
+ isNullable,
21
+ isObject,
22
+ isObjectRef,
23
+ isPrunePoint,
24
+ isRef,
25
+ maybeGetOid,
26
+ memoByKeys,
27
+ traverseCollectionFieldsAndApplyDefaults,
28
+ validateEntityField,
29
+ } from '@verdant-web/common';
30
+ import { Context } from '../../context.js';
31
+ import { FileManager } from '../../files/FileManager.js';
32
+ import { isFile, processValueFiles } from '../../files/utils.js';
33
+ import { EntityFile } from '../../index.js';
34
+ import { EntityCache } from './EntityCache.js';
35
+ import { EntityFamilyMetadata, EntityMetadataView } from './EntityMetadata.js';
36
+ import {
37
+ BaseEntityValue,
38
+ DataFromInit,
39
+ DeepPartial,
40
+ EntityChange,
41
+ EntityEvents,
42
+ ListEntity,
43
+ ListItemInit,
44
+ ListItemValue,
45
+ ObjectEntity,
46
+ } from './types.js';
47
+ import { EntityStoreEventData, EntityStoreEvents } from './EntityStore.js';
48
+
49
+ export interface EntityInit {
50
+ oid: ObjectIdentifier;
51
+ schema: StorageFieldSchema;
52
+ entityFamily?: EntityCache;
53
+ metadataFamily: EntityFamilyMetadata;
54
+ parent?: Entity;
55
+ ctx: Context;
56
+ files: FileManager;
57
+ readonlyKeys?: string[];
58
+ fieldPath?: (string | number)[];
59
+ patchCreator: PatchCreator;
60
+ events: EntityStoreEvents;
61
+ }
62
+
63
+ export class Entity<
64
+ Init = any,
65
+ KeyValue extends BaseEntityValue = any,
66
+ Snapshot extends any = DataFromInit<Init>,
67
+ >
68
+ extends EventSubscriber<EntityEvents>
69
+ implements
70
+ ObjectEntity<Init, KeyValue, Snapshot>,
71
+ ListEntity<Init, KeyValue, Snapshot>
72
+ {
73
+ readonly oid: ObjectIdentifier;
74
+ private readonlyKeys: string[];
75
+ private fieldPath: (string | number)[] = [];
76
+ // these are shared between all entities in this family
77
+ private entityFamily: EntityCache;
78
+ private metadataFamily;
79
+
80
+ private schema;
81
+ private parent: Entity | undefined;
82
+ private ctx;
83
+ private files;
84
+ private patchCreator;
85
+ private events;
86
+
87
+ // an internal representation of this Entity.
88
+ // if present, this is the cached, known value. If null,
89
+ // the entity is deleted. If undefined, we need to recompute
90
+ // the view.
91
+ private _viewData: EntityMetadataView | undefined = undefined;
92
+ private validationError: EntityValidationProblem | undefined = undefined;
93
+ private cachedDeepUpdatedAt: number | null = null;
94
+ // only used for root entities to track delete/restore state.
95
+ private wasDeletedLastChange = false;
96
+ private cachedView: any | undefined = undefined;
97
+
98
+ constructor({
99
+ oid,
100
+ schema,
101
+ entityFamily: childCache,
102
+ parent,
103
+ ctx,
104
+ metadataFamily,
105
+ readonlyKeys,
106
+ files,
107
+ patchCreator,
108
+ events,
109
+ }: EntityInit) {
110
+ super();
111
+
112
+ assert(!!oid, 'oid is required');
113
+
114
+ this.oid = oid;
115
+ this.readonlyKeys = readonlyKeys || [];
116
+ this.ctx = ctx;
117
+ this.files = files;
118
+ this.schema = schema;
119
+ this.entityFamily =
120
+ childCache ||
121
+ new EntityCache({
122
+ initial: [this],
123
+ });
124
+ this.patchCreator = patchCreator;
125
+ this.metadataFamily = metadataFamily;
126
+ this.events = events;
127
+ this.parent = parent;
128
+
129
+ // TODO: should any but the root entity be listening to these?
130
+ if (!this.parent) {
131
+ events.add.attach(this.onAdd);
132
+ events.replace.attach(this.onReplace);
133
+ events.resetAll.attach(this.onResetAll);
134
+ }
135
+ }
136
+
137
+ private onAdd = (_store: any, data: EntityStoreEventData) => {
138
+ if (data.oid === this.oid) {
139
+ this.addConfirmedData(data);
140
+ }
141
+ };
142
+ private onReplace = (_store: any, data: EntityStoreEventData) => {
143
+ if (data.oid === this.oid) {
144
+ this.replaceAllData(data);
145
+ }
146
+ };
147
+ private onResetAll = () => {
148
+ this.resetAllData();
149
+ };
150
+
151
+ private get metadata() {
152
+ return this.metadataFamily.get(this.oid);
153
+ }
154
+
155
+ /**
156
+ * The view of this Entity, not including nested
157
+ * entities (that's the snapshot - see #getSnapshot())
158
+ *
159
+ * Nested entities are represented by refs.
160
+ */
161
+ private get viewData() {
162
+ if (this._viewData === undefined) {
163
+ this._viewData = this.metadata.computeView();
164
+ this.validate();
165
+ }
166
+ return this._viewData;
167
+ }
168
+
169
+ /** convenience getter for viewData.view */
170
+ private get rawView() {
171
+ return this.viewData.view;
172
+ }
173
+
174
+ /**
175
+ * An Entity's View includes the rendering of its underlying data,
176
+ * connecting of children where refs were, and validation
177
+ * and pruning according to schema.
178
+ */
179
+ private get view() {
180
+ if (this.cachedView !== undefined) {
181
+ return this.cachedView;
182
+ }
183
+
184
+ if (this.viewData.deleted) {
185
+ return null;
186
+ }
187
+ // can't use invalid data - but this should be bubbled up to
188
+ // a prune point
189
+ const rawView = this.rawView;
190
+
191
+ const viewIsWrongType =
192
+ (!rawView && !isNullable(this.schema)) ||
193
+ (this.schema.type === 'array' && !Array.isArray(rawView)) ||
194
+ ((this.schema.type === 'object' || this.schema.type === 'map') &&
195
+ !isObject(rawView));
196
+
197
+ if (viewIsWrongType) {
198
+ // this will cover lists and maps, too.
199
+ if (hasDefault(this.schema)) {
200
+ return getDefault(this.schema);
201
+ }
202
+ // force null - invalid - will require parent prune
203
+ return null as any;
204
+ }
205
+
206
+ this.cachedView = this.isList ? [] : {};
207
+ assignOid(this.cachedView, this.oid);
208
+
209
+ if (Array.isArray(rawView)) {
210
+ const schema = getChildFieldSchema(this.schema, 0);
211
+ if (!schema) {
212
+ /**
213
+ * PRUNE - this is a prune point. we can't continue
214
+ * to render this data, so we'll just return [].
215
+ * This skips the loop.
216
+ */
217
+ this.ctx.log(
218
+ 'error',
219
+ 'No child field schema for list entity.',
220
+ this.oid,
221
+ );
222
+ } else {
223
+ for (let i = 0; i < rawView.length; i++) {
224
+ const child = this.get(i);
225
+ if (this.childIsNull(child) && !isNullable(schema)) {
226
+ this.ctx.log(
227
+ 'error',
228
+ 'Child missing in non-nullable field',
229
+ this.oid,
230
+ 'index:',
231
+ i,
232
+ );
233
+
234
+ // this item will be pruned.
235
+ } else {
236
+ this.cachedView.push(child);
237
+ }
238
+ }
239
+ }
240
+ } else if (isObject(rawView)) {
241
+ // iterate over known properties in object-type entities;
242
+ // for maps, we just iterate over the keys.
243
+ const keys =
244
+ this.schema.type === 'object'
245
+ ? Object.keys(this.schema.properties)
246
+ : Object.keys(rawView);
247
+ for (const key of keys) {
248
+ const schema = getChildFieldSchema(this.schema, key);
249
+ if (!schema) {
250
+ /**
251
+ * PRUNE - this is a prune point. we can't continue
252
+ * to render this data. If this is a map, it will be
253
+ * pruned empty. Otherwise, prune moves upward.
254
+ *
255
+ * This exits the loop.
256
+ */
257
+ this.ctx.log(
258
+ 'error',
259
+ 'No child field schema for object entity at key',
260
+ key,
261
+ );
262
+ if (this.schema.type === 'map') {
263
+ // it's valid to prune here if it's a map
264
+ this.cachedView = {};
265
+ } else {
266
+ // otherwise prune moves upward
267
+ this.cachedView = null;
268
+ }
269
+ break;
270
+ }
271
+ const child = this.get(key as any);
272
+ if (this.childIsNull(child) && !isNullable(schema)) {
273
+ this.ctx.log(
274
+ 'error',
275
+ 'Child entity is missing for non-nullable field',
276
+ this.oid,
277
+ 'key:',
278
+ key,
279
+ );
280
+ /**
281
+ * PRUNE - this is a prune point. we can't continue
282
+ * to render this data. If this is a map, we can ignore
283
+ * this value. Otherwise we must prune upward.
284
+ * This exits the loop.
285
+ */
286
+ if (this.schema.type !== 'map') {
287
+ this.cachedView = null;
288
+ break;
289
+ }
290
+ } else {
291
+ this.cachedView[key] = child;
292
+ }
293
+ }
294
+ }
295
+
296
+ return this.cachedView;
297
+ }
298
+
299
+ private childIsNull = (child: any) => {
300
+ if (child instanceof Entity) {
301
+ const childView = child.view;
302
+ return childView === null || childView === undefined;
303
+ }
304
+ return child === null || child === undefined;
305
+ };
306
+
307
+ get uid() {
308
+ return this.oid;
309
+ }
310
+
311
+ get deleted() {
312
+ return this.viewData.deleted || this.view === null;
313
+ }
314
+
315
+ get invalid() {
316
+ return !!this.validate();
317
+ }
318
+
319
+ get isList() {
320
+ // have to turn TS off here as our two interfaces both implement
321
+ // const values for this boolean.
322
+ return (
323
+ this.schema.type === 'array' || (Array.isArray(this.viewData.view) as any)
324
+ );
325
+ }
326
+
327
+ get updatedAt() {
328
+ return this.viewData.updatedAt;
329
+ }
330
+
331
+ get deepUpdatedAt() {
332
+ if (this.cachedDeepUpdatedAt) return this.cachedDeepUpdatedAt;
333
+ // iterate over all children and take the latest timestamp
334
+ let latest: number | null = this.updatedAt;
335
+ if (this.isList) {
336
+ this.forEach((child: any) => {
337
+ if (child instanceof Entity) {
338
+ const childTimestamp = child.deepUpdatedAt;
339
+ if (childTimestamp && (!latest || childTimestamp > latest)) {
340
+ latest = childTimestamp;
341
+ }
342
+ }
343
+ });
344
+ } else {
345
+ this.values().forEach((child) => {
346
+ if (child instanceof Entity) {
347
+ const childTimestamp = child.deepUpdatedAt;
348
+ if (childTimestamp && (!latest || childTimestamp > latest)) {
349
+ latest = childTimestamp;
350
+ }
351
+ }
352
+ });
353
+ }
354
+ this.cachedDeepUpdatedAt = latest;
355
+ return latest;
356
+ }
357
+
358
+ /**
359
+ * @internal - this is relevant to Verdant's system, not users.
360
+ *
361
+ * Indicates whether this document is from an outdated version
362
+ * of the schema - which means it cannot be used until it is upgraded.
363
+ */
364
+ get isOutdatedVersion(): boolean {
365
+ if (this.parent) return this.parent.isOutdatedVersion;
366
+ return this.viewData.fromOlderVersion;
367
+ }
368
+
369
+ /**
370
+ * Pruning - when entities have invalid children, we 'prune' that
371
+ * data up to the nearest prunable point - a nullable field,
372
+ * or a list.
373
+ */
374
+ protected validate = memoByKeys(
375
+ () => {
376
+ this.validationError =
377
+ validateEntityField({
378
+ field: this.schema,
379
+ value: this.rawView,
380
+ fieldPath: this.fieldPath,
381
+ depth: 1,
382
+ }) ?? undefined;
383
+ return this.validationError;
384
+ },
385
+ () => [this.viewData],
386
+ );
387
+
388
+ private viewWithMappedChildren = (
389
+ mapper: (child: Entity | EntityFile) => any,
390
+ ) => {
391
+ const view = this.view;
392
+ if (!view) {
393
+ return null;
394
+ }
395
+ if (Array.isArray(view)) {
396
+ const mapped = view.map((value) => {
397
+ if (value instanceof Entity || value instanceof EntityFile) {
398
+ return mapper(value);
399
+ } else {
400
+ return value;
401
+ }
402
+ });
403
+ assignOid(mapped, this.oid);
404
+ return mapped;
405
+ } else {
406
+ const mapped = Object.entries(view).reduce((acc, [key, value]) => {
407
+ if (value instanceof Entity || value instanceof EntityFile) {
408
+ acc[key as any] = mapper(value);
409
+ } else {
410
+ acc[key as any] = value;
411
+ }
412
+ return acc;
413
+ }, {} as any);
414
+ assignOid(mapped, this.oid);
415
+ return mapped;
416
+ }
417
+ };
418
+
419
+ /**
420
+ * A current snapshot of this Entity's data, including nested
421
+ * Entities.
422
+ */
423
+ getSnapshot = (): any => {
424
+ return this.viewWithMappedChildren((child) => child.getSnapshot());
425
+ };
426
+
427
+ // change management methods (internal use only)
428
+ private addPendingOperations = (operations: Operation[]) => {
429
+ this.ctx.log('debug', 'Entity: adding pending operations', this.oid);
430
+ const changes = this.metadataFamily.addPendingData(operations);
431
+ for (const change of changes) {
432
+ this.change(change);
433
+ }
434
+ };
435
+
436
+ private addConfirmedData = (data: EntityStoreEventData) => {
437
+ this.ctx.log('debug', 'Entity: adding confirmed data', this.oid);
438
+ const changes = this.metadataFamily.addConfirmedData(data);
439
+ for (const change of changes) {
440
+ this.change(change);
441
+ }
442
+ };
443
+
444
+ private replaceAllData = (data: EntityStoreEventData) => {
445
+ this.ctx.log('debug', 'Entity: replacing all data', this.oid);
446
+ const changes = this.metadataFamily.replaceAllData(data);
447
+ for (const change of changes) {
448
+ this.change(change);
449
+ }
450
+ };
451
+
452
+ private resetAllData = () => {
453
+ this.ctx.log('debug', 'Entity: resetting all data', this.oid);
454
+ this.cachedDeepUpdatedAt = null;
455
+ this.cachedView = undefined;
456
+ this._viewData = undefined;
457
+ const changes = this.metadataFamily.replaceAllData({});
458
+ for (const change of changes) {
459
+ this.change(change);
460
+ }
461
+ };
462
+
463
+ private change = (ev: EntityChange) => {
464
+ if (ev.oid === this.oid) {
465
+ // reset cached view
466
+ this._viewData = undefined;
467
+ this.cachedView = undefined;
468
+ // chain deepChanges to parents
469
+ this.deepChange(this, ev);
470
+ // emit the change, it's for us
471
+ this.ctx.log('Emitting change event', this.oid);
472
+ this.emit('change', { isLocal: ev.isLocal });
473
+ // for root entities, we need to go ahead and decide if we're
474
+ // deleted or not - so queries can exclude us if we are.
475
+ if (!this.parent) {
476
+ // newly deleted - emit event
477
+ if (this.deleted && !this.wasDeletedLastChange) {
478
+ this.ctx.log('debug', 'Entity deleted', this.oid);
479
+ this.emit('delete', { isLocal: ev.isLocal });
480
+ this.wasDeletedLastChange = true;
481
+ } else if (!this.deleted && this.wasDeletedLastChange) {
482
+ this.ctx.log('debug', 'Entity restored', this.oid);
483
+ // newly restored - emit event
484
+ this.emit('restore', { isLocal: ev.isLocal });
485
+ this.wasDeletedLastChange = false;
486
+ }
487
+ }
488
+ } else {
489
+ // forward it to the correct family member. if none exists
490
+ // in cache, no one will hear it anyways.
491
+ const other = this.entityFamily.getCached(ev.oid);
492
+ if (other && other instanceof Entity) {
493
+ other.change(ev);
494
+ }
495
+ }
496
+ };
497
+ protected deepChange = (target: Entity, ev: EntityChange) => {
498
+ // reset cached deep updated at timestamp; either this
499
+ // entity or children have changed
500
+ this.cachedDeepUpdatedAt = null;
501
+ // reset this flag to recompute snapshot data - children
502
+ // or self has changed. new pruning needs to happen.
503
+ this.cachedView = undefined;
504
+ this.ctx.log(
505
+ 'debug',
506
+ 'Deep change detected at',
507
+ this.oid,
508
+ 'reset cached view',
509
+ );
510
+ this.ctx.log('debug', 'Emitting deep change event', this.oid);
511
+ this.emit('changeDeep', target, ev);
512
+ this.parent?.deepChange(target, ev);
513
+ };
514
+
515
+ private getChild = (key: any, oid: ObjectIdentifier) => {
516
+ const schema = getChildFieldSchema(this.schema, key);
517
+ if (!schema) {
518
+ throw new Error(
519
+ `No schema for key ${String(key)} in ${JSON.stringify(this.schema)}`,
520
+ );
521
+ }
522
+ return this.entityFamily.get({
523
+ oid,
524
+ schema,
525
+ entityFamily: this.entityFamily,
526
+ metadataFamily: this.metadataFamily,
527
+ parent: this,
528
+ ctx: this.ctx,
529
+ files: this.files,
530
+ fieldPath: [...this.fieldPath, key],
531
+ patchCreator: this.patchCreator,
532
+ events: this.events,
533
+ });
534
+ };
535
+
536
+ // generic entity methods
537
+ /**
538
+ * Gets a value from this Entity. If the value
539
+ * is an object, it will be wrapped in another
540
+ * Entity.
541
+ */
542
+ get = <Key extends keyof KeyValue>(key: Key): KeyValue[Key] => {
543
+ assertNotSymbol(key);
544
+
545
+ const view = this.rawView;
546
+ if (!view) {
547
+ throw new Error(
548
+ `Cannot access data at key ${key} on deleted entity ${this.oid}`,
549
+ );
550
+ }
551
+ const child = view[key as any];
552
+ const schema = getChildFieldSchema(this.schema, key);
553
+ if (!schema) {
554
+ throw new Error(
555
+ `No schema for key ${String(key)} in ${JSON.stringify(this.schema)}`,
556
+ );
557
+ }
558
+ if (isRef(child)) {
559
+ if (isFileRef(child)) {
560
+ if (schema.type !== 'file') {
561
+ throw new Error(
562
+ `Expected file schema for key ${String(key)}, got ${schema.type}`,
563
+ );
564
+ }
565
+ const file = this.files.get(child.id, {
566
+ downloadRemote: !!schema.downloadRemote,
567
+ });
568
+
569
+ // FIXME: this seems bad and inconsistent
570
+ file.subscribe('change', () => {
571
+ this.deepChange(this, { isLocal: false, oid: this.oid });
572
+ });
573
+
574
+ return file as KeyValue[Key];
575
+ } else {
576
+ return this.getChild(key, child.id) as KeyValue[Key];
577
+ }
578
+ } else {
579
+ // prune invalid primitive fields
580
+ if (
581
+ validateEntityField({
582
+ field: schema,
583
+ value: child,
584
+ fieldPath: [...this.fieldPath, key],
585
+ depth: 1,
586
+ requireDefaults: true,
587
+ })
588
+ ) {
589
+ if (hasDefault(schema)) {
590
+ return getDefault(schema);
591
+ }
592
+ if (isNullable(schema)) {
593
+ return null as any;
594
+ }
595
+ return undefined as any;
596
+ }
597
+ return child as KeyValue[Key];
598
+ }
599
+ };
600
+
601
+ private processInputValue = (value: any, key: any) => {
602
+ if (this.readonlyKeys.includes(key as string)) {
603
+ throw new Error(`Cannot set readonly key ${key.toString()}`);
604
+ }
605
+ // disassociate incoming OIDs on values and generally break object
606
+ // references. cloning doesn't work on files so those are
607
+ // filtered out.
608
+ // The goal here is to be safe about a bunch of cases that could
609
+ // result in corrupt data, like...
610
+ // ent1.set('objField', ent2.get('objField'))
611
+ // or
612
+ // var shared = { foo: 'bar' };
613
+ // ent1.set('objField', shared);
614
+ // ent2.set('objField', shared);
615
+ // ... each of these would result in the same object being
616
+ // referenced in multiple entities, which could mean introduction
617
+ // of foreign OIDs, or one object being assigned different OIDs
618
+ // with unexpected results.
619
+ if (!isFile(value)) {
620
+ value = cloneDeep(value, false);
621
+ }
622
+ const fieldSchema = getChildFieldSchema(this.schema, key);
623
+ if (fieldSchema) {
624
+ traverseCollectionFieldsAndApplyDefaults(value, fieldSchema);
625
+ const validationError = validateEntityField({
626
+ field: fieldSchema,
627
+ value,
628
+ fieldPath: [...this.fieldPath, key],
629
+ });
630
+ if (validationError) {
631
+ // TODO: is it a good idea to throw an error here? a runtime error won't be that helpful,
632
+ // but also we don't really want invalid data supplied.
633
+ throw new Error(validationError.message);
634
+ }
635
+ }
636
+ return processValueFiles(value, this.files.add);
637
+ };
638
+
639
+ private getDeleteMode = (key: any) => {
640
+ if (this.readonlyKeys.includes(key)) {
641
+ return false;
642
+ }
643
+ // any is always deletable, and map values
644
+ if (this.schema.type === 'any' || this.schema.type === 'map') {
645
+ return 'delete';
646
+ }
647
+
648
+ if (this.schema.type === 'object') {
649
+ const property = this.schema.properties[key];
650
+ if (!property) {
651
+ // huh, the property doesn't exist. it's ok to
652
+ // remove I suppose.
653
+ return 'delete';
654
+ }
655
+ if (property.type === 'any') return 'delete';
656
+ // map can't be nullable. should it be?
657
+ if (property.type === 'map') return false;
658
+ if (property.nullable) return 'null';
659
+ }
660
+ // no other types are deletable
661
+ return false;
662
+ };
663
+
664
+ /**
665
+ * Returns the referent value of an item in the list, used for
666
+ * operations which act on items. if the item is an object,
667
+ * it will attempt to create an OID reference to it. If it
668
+ * is a primitive, it will return the primitive.
669
+ */
670
+ private getItemRefValue = (item: any) => {
671
+ if (item instanceof Entity) {
672
+ return createRef(item.oid);
673
+ }
674
+ if (item instanceof EntityFile) {
675
+ return createFileRef(item.id);
676
+ }
677
+ if (typeof item === 'object') {
678
+ const itemOid = maybeGetOid(item);
679
+ if (!itemOid || !this.entityFamily.has(itemOid)) {
680
+ throw new Error(
681
+ `Cannot move object ${JSON.stringify(
682
+ item,
683
+ )} which does not exist in this list`,
684
+ );
685
+ }
686
+ return createRef(itemOid);
687
+ } else {
688
+ return item;
689
+ }
690
+ };
691
+
692
+ set = <Key extends keyof Init>(key: Key, value: Init[Key]) => {
693
+ assertNotSymbol(key);
694
+ this.addPendingOperations(
695
+ this.patchCreator.createSet(
696
+ this.oid,
697
+ key,
698
+ this.processInputValue(value, key),
699
+ ),
700
+ );
701
+ };
702
+
703
+ /**
704
+ * Returns a destructured version of this Entity, where child
705
+ * Entities are accessible at their respective keys.
706
+ */
707
+ getAll = (): KeyValue => {
708
+ return this.view;
709
+ };
710
+
711
+ delete = (key: any) => {
712
+ if (this.isList) {
713
+ assertNumber(key);
714
+ this.addPendingOperations(
715
+ this.patchCreator.createListDelete(this.oid, key),
716
+ );
717
+ } else {
718
+ // the key must be deletable - i.e. optional in the schema.
719
+ const deleteMode = this.getDeleteMode(key);
720
+ if (!deleteMode) {
721
+ throw new Error(
722
+ `Cannot delete key ${key.toString()} - the property is not marked as optional in the schema.`,
723
+ );
724
+ }
725
+ if (deleteMode === 'delete') {
726
+ this.addPendingOperations(
727
+ this.patchCreator.createRemove(this.oid, key),
728
+ );
729
+ } else {
730
+ this.addPendingOperations(
731
+ this.patchCreator.createSet(this.oid, key, null),
732
+ );
733
+ }
734
+ }
735
+ };
736
+
737
+ // object entity methods
738
+ keys = (): string[] => {
739
+ if (!this.view) return [];
740
+ return Object.keys(this.view);
741
+ };
742
+
743
+ entries = (): [string, Exclude<KeyValue[keyof KeyValue], undefined>][] => {
744
+ if (!this.view) return [];
745
+ return Object.entries(this.view);
746
+ };
747
+
748
+ values = (): Exclude<KeyValue[keyof KeyValue], undefined>[] => {
749
+ if (!this.view) return [];
750
+ return Object.values(this.view);
751
+ };
752
+
753
+ update = (
754
+ data: DeepPartial<Init>,
755
+ {
756
+ merge = true,
757
+ replaceSubObjects = false,
758
+ }: { replaceSubObjects?: boolean; merge?: boolean } = {},
759
+ ): void => {
760
+ if (!merge && this.schema.type !== 'any' && this.schema.type !== 'map') {
761
+ throw new Error(
762
+ 'Cannot use .update without merge if the field has a strict schema type. merge: false is only available on "any" or "map" types.',
763
+ );
764
+ }
765
+ const changes: any = {};
766
+ assignOid(changes, this.oid);
767
+ for (const [key, field] of Object.entries(data)) {
768
+ if (this.readonlyKeys.includes(key as any)) {
769
+ throw new Error(`Cannot set readonly key ${key.toString()}`);
770
+ }
771
+ const fieldSchema = getChildFieldSchema(this.schema, key);
772
+ if (fieldSchema) {
773
+ traverseCollectionFieldsAndApplyDefaults(field, fieldSchema);
774
+ }
775
+ changes[key] = this.processInputValue(field, key);
776
+ }
777
+ this.addPendingOperations(
778
+ this.patchCreator.createDiff(this.getSnapshot(), changes, {
779
+ mergeUnknownObjects: !replaceSubObjects,
780
+ defaultUndefined: merge,
781
+ }),
782
+ );
783
+ };
784
+
785
+ // array entity methods
786
+ get length(): number {
787
+ return this.view.length;
788
+ }
789
+
790
+ push = (value: ListItemInit<Init>): void => {
791
+ this.addPendingOperations(
792
+ this.patchCreator.createListPush(
793
+ this.oid,
794
+ this.processInputValue(value, this.view.length),
795
+ ),
796
+ );
797
+ };
798
+
799
+ insert = (index: number, value: ListItemInit<Init>): void => {
800
+ this.addPendingOperations(
801
+ this.patchCreator.createListInsert(
802
+ this.oid,
803
+ index,
804
+ this.processInputValue(value, index),
805
+ ),
806
+ );
807
+ };
808
+
809
+ move = (from: number, to: number): void => {
810
+ this.addPendingOperations(
811
+ this.patchCreator.createListMoveByIndex(this.oid, from, to),
812
+ );
813
+ };
814
+
815
+ moveItem = (item: ListItemValue<KeyValue>, to: number): void => {
816
+ const itemRef = this.getItemRefValue(item);
817
+ if (isRef(itemRef)) {
818
+ this.addPendingOperations(
819
+ this.patchCreator.createListMoveByRef(this.oid, itemRef, to),
820
+ );
821
+ } else {
822
+ const index = this.view.indexOf(item);
823
+ if (index === -1) {
824
+ throw new Error(
825
+ `Cannot move item ${JSON.stringify(
826
+ item,
827
+ )} which does not exist in this list`,
828
+ );
829
+ }
830
+ this.move(index, to);
831
+ }
832
+ };
833
+
834
+ add = (value: ListItemValue<KeyValue>): void => {
835
+ this.addPendingOperations(
836
+ this.patchCreator.createListAdd(
837
+ this.oid,
838
+ this.processInputValue(value, this.view.length),
839
+ ),
840
+ );
841
+ };
842
+
843
+ removeAll = (item: ListItemValue<KeyValue>): void => {
844
+ this.addPendingOperations(
845
+ this.patchCreator.createListRemove(this.oid, this.getItemRefValue(item)),
846
+ );
847
+ };
848
+
849
+ removeFirst = (item: ListItemValue<KeyValue>): void => {
850
+ this.addPendingOperations(
851
+ this.patchCreator.createListRemove(
852
+ this.oid,
853
+ this.getItemRefValue(item),
854
+ 'first',
855
+ ),
856
+ );
857
+ };
858
+
859
+ removeLast = (item: ListItemValue<KeyValue>): void => {
860
+ this.addPendingOperations(
861
+ this.patchCreator.createListRemove(
862
+ this.oid,
863
+ this.getItemRefValue(item),
864
+ 'last',
865
+ ),
866
+ );
867
+ };
868
+
869
+ // list implements an iterator which maps items to wrapped
870
+ // versions
871
+ [Symbol.iterator]() {
872
+ let index = 0;
873
+ let length = this.view?.length;
874
+ return {
875
+ next: () => {
876
+ if (index < length) {
877
+ return {
878
+ value: this.get(index++) as ListItemValue<KeyValue>,
879
+ done: false,
880
+ } as const;
881
+ }
882
+ return {
883
+ value: undefined,
884
+ done: true,
885
+ } as const;
886
+ },
887
+ };
888
+ }
889
+
890
+ map = <U>(
891
+ callback: (value: ListItemValue<KeyValue>, index: number) => U,
892
+ ): U[] => {
893
+ return this.view.map(callback);
894
+ };
895
+
896
+ filter = (
897
+ callback: (value: ListItemValue<KeyValue>, index: number) => boolean,
898
+ ): ListItemValue<KeyValue>[] => {
899
+ return this.view.filter(callback);
900
+ };
901
+
902
+ has = (value: ListItemValue<KeyValue>): boolean => {
903
+ if (!this.isList) {
904
+ throw new Error('has() is only available on list entities');
905
+ }
906
+ const itemRef = this.getItemRefValue(value);
907
+ if (isRef(itemRef)) {
908
+ return this.view.some((item: any) => {
909
+ if (isRef(item)) {
910
+ return compareRefs(item, itemRef);
911
+ }
912
+ });
913
+ } else {
914
+ return this.view.includes(value);
915
+ }
916
+ };
917
+
918
+ forEach = (
919
+ callback: (value: ListItemValue<KeyValue>, index: number) => void,
920
+ ): void => {
921
+ this.view.forEach(callback);
922
+ };
923
+
924
+ some = (predicate: (value: ListItemValue<KeyValue>) => boolean): boolean => {
925
+ return this.view.some(predicate);
926
+ };
927
+
928
+ every = (predicate: (value: ListItemValue<KeyValue>) => boolean): boolean => {
929
+ return this.view.every(predicate);
930
+ };
931
+
932
+ find = (
933
+ predicate: (value: ListItemValue<KeyValue>) => boolean,
934
+ ): ListItemValue<KeyValue> | undefined => {
935
+ return this.view.find(predicate);
936
+ };
937
+
938
+ includes = this.has;
939
+
940
+ // TODO: make these escape hatches unnecessary
941
+ __getViewData__ = (oid: ObjectIdentifier, type: 'confirmed' | 'pending') => {
942
+ return this.metadataFamily.get(oid).computeView(type === 'confirmed');
943
+ };
944
+ __getFamilyOids__ = () => this.metadataFamily.getAllOids();
945
+ }
946
+
947
+ function assertNotSymbol<T>(key: T): asserts key is Exclude<T, symbol> {
948
+ if (typeof key === 'symbol') throw new Error("Symbol keys aren't supported");
949
+ }
950
+
951
+ function assertNumber(key: unknown): asserts key is number {
952
+ if (typeof key !== 'number')
953
+ throw new Error('Only number keys are supported in list entities');
954
+ }