@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
@@ -1,185 +1,340 @@
1
1
  import {
2
+ DocumentBaseline,
3
+ EntityValidationProblem,
4
+ EventSubscriber,
5
+ ObjectIdentifier,
6
+ Operation,
7
+ PatchCreator,
8
+ StorageFieldSchema,
9
+ StorageFieldsSchema,
2
10
  assert,
3
11
  assignOid,
4
12
  cloneDeep,
13
+ compareRefs,
14
+ createFileRef,
5
15
  createRef,
6
- decomposeOid,
7
- EventSubscriber,
8
- FileData,
9
- FileRef,
16
+ getChildFieldSchema,
17
+ getDefault,
18
+ hasDefault,
10
19
  isFileRef,
20
+ isNullable,
21
+ isObject,
11
22
  isObjectRef,
23
+ isPrunePoint,
24
+ isRef,
12
25
  maybeGetOid,
13
- ObjectIdentifier,
14
- Operation,
15
- PatchCreator,
16
- StorageFieldSchema,
17
- StorageFieldsSchema,
18
- TimestampProvider,
26
+ memoByKeys,
19
27
  traverseCollectionFieldsAndApplyDefaults,
20
28
  validateEntityField,
21
29
  } from '@verdant-web/common';
22
- import { EntityFile } from '../files/EntityFile.js';
23
- import { processValueFiles } from '../files/utils.js';
24
-
25
- export const ADD_OPERATIONS = '@@addOperations';
26
- export const DELETE = '@@delete';
27
- export const REBASE = '@@rebase';
28
- const REFRESH = '@@refresh';
29
- export const DEEP_CHANGE = '@@deepChange';
30
-
31
- export interface CacheTools {
32
- computeView(oid: ObjectIdentifier): {
33
- view: any;
34
- deleted: boolean;
35
- lastTimestamp: number | null;
36
- };
37
- getEntity(params: {
38
- oid: ObjectIdentifier;
39
- fieldSchema: StorageFieldSchema;
40
- parent?: Entity;
41
- fieldKey?: string | number;
42
- }): Entity;
43
- hasOid(oid: ObjectIdentifier): boolean;
44
- weakRef<T extends object>(value: T): WeakRef<T>;
45
- }
46
-
47
- export interface StoreTools {
48
- addLocalOperations(operations: Operation[]): void;
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)[];
49
59
  patchCreator: PatchCreator;
50
- addFile: (file: FileData) => void;
51
- getFile: (id: string) => EntityFile;
52
- time: TimestampProvider;
53
- now: string;
54
- }
55
-
56
- export type AccessibleEntityProperty<T> = T extends Array<any>
57
- ? number
58
- : T extends object
59
- ? keyof T
60
- : never;
61
-
62
- type DataFromInit<Init> = Init extends { [key: string]: any }
63
- ? {
64
- [Key in keyof Init]: Init[Key];
65
- }
66
- : Init extends Array<any>
67
- ? Init
68
- : any;
69
-
70
- export type EntityShape<E extends Entity<any, any>> = E extends Entity<
71
- infer Value,
72
- any
73
- >
74
- ? Value
75
- : never;
76
-
77
- // reduces keys of an object to only ones with an optional
78
- // value
79
- type DeletableKeys<T> = keyof {
80
- [Key in keyof T as IfNullableThen<T[Key], Key>]: Key;
81
- };
82
- type IfNullableThen<T, Out> = undefined extends T
83
- ? Out
84
- : null extends T
85
- ? Out
86
- : never;
87
-
88
- export function refreshEntity(
89
- entity: Entity<any, any>,
90
- info: EntityChangeInfo,
91
- ) {
92
- return entity[REFRESH](info);
93
- }
94
-
95
- export interface EntityChangeInfo {
96
- isLocal?: boolean;
60
+ events: EntityStoreEvents;
97
61
  }
98
62
 
99
- type EntityEvents = {
100
- change: (info: EntityChangeInfo) => void;
101
- changeDeep: (target: Entity<any, any>, info: EntityChangeInfo) => void;
102
- delete: (info: EntityChangeInfo) => void;
103
- restore: (info: EntityChangeInfo) => void;
104
- };
105
-
106
- type BaseEntityValue = { [Key: string]: any } | any[];
107
-
108
63
  export class Entity<
109
64
  Init = any,
110
65
  KeyValue extends BaseEntityValue = any,
111
66
  Snapshot extends any = DataFromInit<Init>,
112
67
  >
68
+ extends EventSubscriber<EntityEvents>
113
69
  implements
114
70
  ObjectEntity<Init, KeyValue, Snapshot>,
115
71
  ListEntity<Init, KeyValue, Snapshot>
116
72
  {
117
- // if current is null, the entity was deleted.
118
- protected _current: any | null = null;
119
-
120
73
  readonly oid: ObjectIdentifier;
121
- readonly fieldPath: string[];
122
- readonly collection: string;
123
- protected readonly store: StoreTools;
124
- protected readonly fieldSchema;
125
- protected readonly cache: CacheTools;
126
- protected _deleted = false;
127
- protected parent: WeakRef<Entity<any, any>> | undefined;
128
- protected readonly readonlyKeys: (keyof Init)[];
129
-
130
- private cachedSnapshot: any = null;
131
- private cachedDestructure: KeyValue | null = null;
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;
132
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;
133
97
 
134
- private _updatedAt: number | null = null;
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();
135
111
 
136
- protected events;
112
+ assert(!!oid, 'oid is required');
137
113
 
138
- protected hasSubscribersToDeepChanges() {
139
- return this.events.subscriberCount('changeDeep') > 0;
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
+ }
140
135
  }
141
136
 
142
- get hasSubscribers() {
143
- if (this.events.totalSubscriberCount() > 0) {
144
- return true;
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
145
  }
146
+ };
147
+ private onResetAll = () => {
148
+ this.resetAllData();
149
+ };
146
150
 
147
- // even if nobody subscribes directly to this entity, if a parent
148
- // has a deep subscription that counts.
149
- let parent = this.parent?.deref();
150
- while (parent) {
151
- if (parent.hasSubscribersToDeepChanges()) {
152
- return true;
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);
153
201
  }
154
- parent = parent.parent?.deref();
202
+ // force null - invalid - will require parent prune
203
+ return null as any;
155
204
  }
156
205
 
157
- return false;
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;
158
309
  }
159
310
 
160
311
  get deleted() {
161
- return this._deleted;
312
+ return this.viewData.deleted || this.view === null;
162
313
  }
163
314
 
164
- protected get value() {
165
- return this._current;
315
+ get invalid() {
316
+ return !!this.validate();
166
317
  }
167
318
 
168
319
  get isList() {
169
- return Array.isArray(this._current) as any;
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
+ );
170
325
  }
171
326
 
172
327
  get updatedAt() {
173
- return this._updatedAt;
328
+ return this.viewData.updatedAt;
174
329
  }
175
330
 
176
331
  get deepUpdatedAt() {
177
332
  if (this.cachedDeepUpdatedAt) return this.cachedDeepUpdatedAt;
178
333
  // iterate over all children and take the latest timestamp
179
- let latest: number | null = this._updatedAt;
334
+ let latest: number | null = this.updatedAt;
180
335
  if (this.isList) {
181
- this.forEach((child) => {
182
- if ((child as any) instanceof Entity) {
336
+ this.forEach((child: any) => {
337
+ if (child instanceof Entity) {
183
338
  const childTimestamp = child.deepUpdatedAt;
184
339
  if (childTimestamp && (!latest || childTimestamp > latest)) {
185
340
  latest = childTimestamp;
@@ -188,7 +343,7 @@ export class Entity<
188
343
  });
189
344
  } else {
190
345
  this.values().forEach((child) => {
191
- if ((child as any) instanceof Entity) {
346
+ if (child instanceof Entity) {
192
347
  const childTimestamp = child.deepUpdatedAt;
193
348
  if (childTimestamp && (!latest || childTimestamp > latest)) {
194
349
  latest = childTimestamp;
@@ -200,170 +355,253 @@ export class Entity<
200
355
  return latest;
201
356
  }
202
357
 
203
- get uid() {
204
- return this.oid;
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;
205
367
  }
206
368
 
207
- constructor({
208
- oid,
209
- store,
210
- fieldSchema,
211
- cache,
212
- parent,
213
- onAllUnsubscribed,
214
- readonlyKeys = [],
215
- fieldPath = [],
216
- }: {
217
- oid: ObjectIdentifier;
218
- store: StoreTools;
219
- fieldSchema: StorageFieldSchema | StorageFieldsSchema;
220
- cache: CacheTools;
221
- parent?: Entity<any, any>;
222
- onAllUnsubscribed?: () => void;
223
- readonlyKeys?: (keyof Init)[];
224
- fieldPath?: string[];
225
- }) {
226
- this.oid = oid;
227
- const { collection } = decomposeOid(oid);
228
- this.collection = collection;
229
- this.store = store;
230
- this.fieldSchema = fieldSchema;
231
- this.fieldPath = fieldPath;
232
- this.readonlyKeys = readonlyKeys;
233
- this.cache = cache;
234
- this.parent = parent && this.cache.weakRef(parent);
235
- const { view, deleted, lastTimestamp } = this.cache.computeView(oid);
236
- this._current = view;
237
- this._deleted = deleted;
238
- this._updatedAt = lastTimestamp ? lastTimestamp : null;
239
- this.cachedDeepUpdatedAt = null;
240
- this.events = new EventSubscriber<EntityEvents>(() => {
241
- if (!this.hasSubscribers) {
242
- onAllUnsubscribed?.();
243
- }
244
- });
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
+ );
245
387
 
246
- if (this.oid.includes('.') && !this.parent) {
247
- throw new Error('Parent must be provided for sub entities');
388
+ private viewWithMappedChildren = (
389
+ mapper: (child: Entity | EntityFile) => any,
390
+ ) => {
391
+ const view = this.view;
392
+ if (!view) {
393
+ return null;
248
394
  }
249
- assert(!!fieldSchema, 'Field schema must be provided');
250
- }
251
-
252
- private [REFRESH] = (info: EntityChangeInfo) => {
253
- const { view, deleted, lastTimestamp } = this.cache.computeView(this.oid);
254
- this._current = view;
255
- const restored = this._deleted && !deleted;
256
- this._deleted = deleted;
257
- this.cachedDestructure = null;
258
- this._updatedAt = lastTimestamp ? lastTimestamp : null;
259
- this.cachedDeepUpdatedAt = null;
260
-
261
- if (this._deleted) {
262
- this.events.emit('delete', info);
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;
263
405
  } else {
264
- this.events.emit('change', info);
265
- this[DEEP_CHANGE](this as unknown as Entity<any, any>, info);
266
- }
267
- if (restored) {
268
- this.cachedSnapshot = null;
269
- this.events.emit('restore', info);
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;
270
416
  }
271
417
  };
272
418
 
273
- private [DEEP_CHANGE] = (
274
- source: Entity<any, any>,
275
- info: EntityChangeInfo,
276
- ) => {
277
- this.cachedSnapshot = null;
278
- this.cachedDeepUpdatedAt = null;
279
- this.events.emit('changeDeep', source, info);
280
- const parent = this.parent?.deref();
281
- if (parent) {
282
- parent[DEEP_CHANGE](source, info);
283
- }
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());
284
425
  };
285
426
 
286
- protected getChildFieldSchema = (key: any) => {
287
- if (this.fieldSchema.type === 'object') {
288
- return this.fieldSchema.properties[key];
289
- } else if (this.fieldSchema.type === 'array') {
290
- return this.fieldSchema.items;
291
- } else if (this.fieldSchema.type === 'map') {
292
- return this.fieldSchema.values;
293
- } else if (this.fieldSchema.type === 'any') {
294
- return this.fieldSchema;
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);
295
433
  }
296
- throw new Error('Invalid field schema');
297
434
  };
298
435
 
299
- dispose = () => {
300
- this.events.dispose();
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
+ }
301
442
  };
302
443
 
303
- subscribe = <EventName extends keyof EntityEvents>(
304
- event: EventName,
305
- callback: EntityEvents[EventName],
306
- ) => {
307
- const unsubscribe = this.events.subscribe(event, callback);
308
-
309
- return unsubscribe;
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
+ }
310
450
  };
311
451
 
312
- protected addPatches = (patches: Operation[]) => {
313
- this.store.addLocalOperations(patches);
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
+ }
314
461
  };
315
462
 
316
- protected cloneCurrent = () => {
317
- if (this._current === undefined) {
318
- return undefined;
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
+ }
319
495
  }
320
- return cloneDeep(this._current);
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);
321
513
  };
322
514
 
323
- protected getSubObject = (
324
- oid: ObjectIdentifier,
325
- key: any,
326
- ): Entity<any, any> => {
327
- const fieldSchema = this.getChildFieldSchema(key);
328
- // this is a failure case, but trying to be graceful about it...
329
- // @ts-ignore
330
- // if (!fieldSchema) return null;
331
- return this.cache.getEntity({
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({
332
523
  oid,
333
- fieldSchema,
524
+ schema,
525
+ entityFamily: this.entityFamily,
526
+ metadataFamily: this.metadataFamily,
334
527
  parent: this,
335
- fieldKey: key,
528
+ ctx: this.ctx,
529
+ files: this.files,
530
+ fieldPath: [...this.fieldPath, key],
531
+ patchCreator: this.patchCreator,
532
+ events: this.events,
336
533
  });
337
534
  };
338
535
 
339
- protected wrapValue = <Key extends keyof KeyValue>(
340
- value: any,
341
- key: Key,
342
- ): KeyValue[Key] => {
343
- if (isObjectRef(value)) {
344
- const oid = value.id;
345
- const subObject = this.getSubObject(oid, key);
346
- if (subObject) {
347
- return subObject as any;
348
- }
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) {
349
547
  throw new Error(
350
- `CACHE MISS: Subobject ${oid} does not exist on ${this.oid}`,
548
+ `Cannot access data at key ${key} on deleted entity ${this.oid}`,
351
549
  );
352
- } else if (isFileRef(value)) {
353
- const file = this.store.getFile(value.id);
354
- if (file) {
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
355
570
  file.subscribe('change', () => {
356
- this[DEEP_CHANGE](this, {
357
- isLocal: false,
358
- });
571
+ this.deepChange(this, { isLocal: false, oid: this.oid });
359
572
  });
360
- return file as any;
573
+
574
+ return file as KeyValue[Key];
575
+ } else {
576
+ return this.getChild(key, child.id) as KeyValue[Key];
361
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];
362
598
  }
363
- return value;
364
599
  };
365
600
 
366
- protected processInputValue = (value: any, key: any) => {
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
+ }
367
605
  // disassociate incoming OIDs on values and generally break object
368
606
  // references. cloning doesn't work on files so those are
369
607
  // filtered out.
@@ -378,350 +616,264 @@ export class Entity<
378
616
  // referenced in multiple entities, which could mean introduction
379
617
  // of foreign OIDs, or one object being assigned different OIDs
380
618
  // with unexpected results.
381
- if (!(value instanceof File)) {
619
+ if (!isFile(value)) {
382
620
  value = cloneDeep(value, false);
383
621
  }
384
- const fieldSchema = this.getChildFieldSchema(key);
622
+ const fieldSchema = getChildFieldSchema(this.schema, key);
385
623
  if (fieldSchema) {
386
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
+ }
387
635
  }
388
- const validationError = validateEntityField(fieldSchema, value, [
389
- ...this.fieldPath,
390
- key,
391
- ]);
392
- if (validationError) {
393
- // TODO: is it a good idea to throw an error here? a runtime error won't be that helpful,
394
- // but also we don't really want invalid data supplied.
395
- throw new Error(validationError);
396
- }
397
- return processValueFiles(value, this.store.addFile);
636
+ return processValueFiles(value, this.files.add);
398
637
  };
399
638
 
400
- get = <Key extends keyof KeyValue>(key: Key): KeyValue[Key] => {
401
- if (this.value === undefined || this.value === null) {
402
- throw new Error('Cannot access deleted entity');
639
+ private getDeleteMode = (key: any) => {
640
+ if (this.readonlyKeys.includes(key)) {
641
+ return false;
403
642
  }
404
-
405
- const value = this.value[key];
406
- return this.wrapValue(value, key);
407
- };
408
-
409
- getAll = (): KeyValue => {
410
- if (this.value === undefined || this.value === null) {
411
- throw new Error('Cannot access deleted entity');
643
+ // any is always deletable, and map values
644
+ if (this.schema.type === 'any' || this.schema.type === 'map') {
645
+ return 'delete';
412
646
  }
413
647
 
414
- if (this.cachedDestructure) return this.cachedDestructure;
415
-
416
- let result: any;
417
- if (Array.isArray(this.value)) {
418
- result = this.value.map((value, index) =>
419
- this.wrapValue(value, index as any),
420
- ) as any;
421
- } else {
422
- result = {} as any;
423
- for (const key in this.value) {
424
- result[key as any] = this.get(key as any);
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';
425
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';
426
659
  }
427
- this.cachedDestructure = result;
428
- return result;
660
+ // no other types are deletable
661
+ return false;
429
662
  };
430
663
 
431
- private getFileSnapshot(item: FileRef) {
432
- const file = this.store.getFile(item.id);
433
- if (file.url) {
434
- return { id: item.id, url: file.url };
435
- } else if (file.loading || file.failed) {
436
- return { id: item.id, url: undefined };
437
- } else {
438
- return { id: item.id, url: null };
439
- }
440
- }
441
-
442
664
  /**
443
- * Returns a copy of the entity and all sub-objects as
444
- * a plain object or array.
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.
445
669
  */
446
- getSnapshot = (): any => {
447
- if (!this.value) {
448
- return null;
670
+ private getItemRefValue = (item: any) => {
671
+ if (item instanceof Entity) {
672
+ return createRef(item.oid);
449
673
  }
450
- if (this.deleted) {
451
- return null;
674
+ if (item instanceof EntityFile) {
675
+ return createFileRef(item.id);
452
676
  }
453
- if (this.cachedSnapshot) {
454
- return this.cachedSnapshot;
455
- }
456
- let snapshot;
457
- if (Array.isArray(this.value)) {
458
- snapshot = this.value.map((item, idx) => {
459
- if (isObjectRef(item)) {
460
- return this.getSubObject(item.id, idx)?.getSnapshot();
461
- } else if (isFileRef(item)) {
462
- return this.getFileSnapshot(item);
463
- }
464
- return item;
465
- }) as Snapshot;
466
- } else {
467
- snapshot = { ...this.value };
468
- for (const [key, value] of Object.entries(snapshot)) {
469
- if (isObjectRef(value)) {
470
- snapshot[key] = this.getSubObject(value.id, key)?.getSnapshot();
471
- } else if (isFileRef(value)) {
472
- snapshot[key] = this.getFileSnapshot(value);
473
- }
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
+ );
474
685
  }
686
+ return createRef(itemOid);
687
+ } else {
688
+ return item;
475
689
  }
476
-
477
- assignOid(snapshot, this.oid);
478
- this.cachedSnapshot = snapshot;
479
- return snapshot;
480
690
  };
481
691
 
482
- /**
483
- * Object methods
484
- */
485
- keys = () => {
486
- return Object.keys(this.value || {});
487
- };
488
- entries = () => {
489
- return Object.entries(this.getAll());
490
- };
491
- values = () => {
492
- return Object.values(this.getAll());
493
- };
494
692
  set = <Key extends keyof Init>(key: Key, value: Init[Key]) => {
495
- if (this.readonlyKeys.includes(key)) {
496
- throw new Error(`Cannot set readonly key ${key.toString()}`);
497
- }
498
- this.addPatches(
499
- this.store.patchCreator.createSet(
693
+ assertNotSymbol(key);
694
+ this.addPendingOperations(
695
+ this.patchCreator.createSet(
500
696
  this.oid,
501
- key as string | number,
697
+ key,
502
698
  this.processInputValue(value, key),
503
699
  ),
504
700
  );
505
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
+
506
711
  delete = (key: any) => {
507
- if (Array.isArray(this.value)) {
508
- this.addPatches(
509
- this.store.patchCreator.createListDelete(this.oid, key as number, 1),
712
+ if (this.isList) {
713
+ assertNumber(key);
714
+ this.addPendingOperations(
715
+ this.patchCreator.createListDelete(this.oid, key),
510
716
  );
511
717
  } else {
512
- // the key must be deletable - i.e. optional in the schema
718
+ // the key must be deletable - i.e. optional in the schema.
513
719
  const deleteMode = this.getDeleteMode(key);
514
720
  if (!deleteMode) {
515
721
  throw new Error(
516
- `Cannot delete key ${key} - the property is not marked as optional in the schema`,
722
+ `Cannot delete key ${key.toString()} - the property is not marked as optional in the schema.`,
517
723
  );
518
724
  }
519
725
  if (deleteMode === 'delete') {
520
- this.addPatches(this.store.patchCreator.createRemove(this.oid, key));
726
+ this.addPendingOperations(
727
+ this.patchCreator.createRemove(this.oid, key),
728
+ );
521
729
  } else {
522
- this.addPatches(this.store.patchCreator.createSet(this.oid, key, null));
730
+ this.addPendingOperations(
731
+ this.patchCreator.createSet(this.oid, key, null),
732
+ );
523
733
  }
524
734
  }
525
735
  };
526
- private getDeleteMode = (key: any) => {
527
- if (this.readonlyKeys.includes(key)) {
528
- return false;
529
- }
530
- // 'any' is always deletable, and map values can be removed completely
531
- if (this.fieldSchema.type === 'any' || this.fieldSchema.type === 'map') {
532
- return 'delete';
533
- }
534
736
 
535
- if (this.fieldSchema.type === 'object') {
536
- const property = this.fieldSchema.properties[key];
537
- if (!property) {
538
- // huh, trying to delete a field that isn't specified
539
- // in the schema. we should use 'delete' mode.
540
- return 'delete';
541
- }
542
- if (property.type === 'any') return 'delete';
543
- // map can't be nullable
544
- // TODO: should it be?
545
- if (property.type === 'map') return false;
546
- // nullable properties can only be set null
547
- if (property.nullable) return 'null';
548
- }
549
- // no other parent objects support deleting
550
- return false;
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);
551
751
  };
552
- /** @deprecated - renamed to delete */
553
- remove = this.delete.bind(this);
554
752
 
555
753
  update = (
556
- value: DeepPartial<Init>,
754
+ data: DeepPartial<Init>,
557
755
  {
558
- replaceSubObjects = false,
559
756
  merge = true,
560
- }: {
561
- /**
562
- * If true, merged sub-objects will be replaced entirely if there's
563
- * ambiguity about their identity.
564
- */
565
- replaceSubObjects?: boolean;
566
- /**
567
- * If false, omitted keys will erase their respective fields.
568
- */
569
- merge?: boolean;
570
- } = {
571
- replaceSubObjects: false,
572
- merge: true,
573
- },
574
- ) => {
575
- if (
576
- !merge &&
577
- this.fieldSchema.type !== 'any' &&
578
- this.fieldSchema.type !== 'map'
579
- ) {
757
+ replaceSubObjects = false,
758
+ }: { replaceSubObjects?: boolean; merge?: boolean } = {},
759
+ ): void => {
760
+ if (!merge && this.schema.type !== 'any' && this.schema.type !== 'map') {
580
761
  throw new Error(
581
762
  'Cannot use .update without merge if the field has a strict schema type. merge: false is only available on "any" or "map" types.',
582
763
  );
583
764
  }
584
- for (const [key, field] of Object.entries(value)) {
765
+ const changes: any = {};
766
+ assignOid(changes, this.oid);
767
+ for (const [key, field] of Object.entries(data)) {
585
768
  if (this.readonlyKeys.includes(key as any)) {
586
769
  throw new Error(`Cannot set readonly key ${key.toString()}`);
587
770
  }
588
- const fieldSchema = this.getChildFieldSchema(key);
771
+ const fieldSchema = getChildFieldSchema(this.schema, key);
589
772
  if (fieldSchema) {
590
773
  traverseCollectionFieldsAndApplyDefaults(field, fieldSchema);
591
774
  }
775
+ changes[key] = this.processInputValue(field, key);
592
776
  }
593
- const withoutFiles = processValueFiles(value, this.store.addFile);
594
- this.addPatches(
595
- this.store.patchCreator.createDiff(
596
- this.getSnapshot(),
597
- assignOid(withoutFiles, this.oid),
598
- {
599
- mergeUnknownObjects: !replaceSubObjects,
600
- defaultUndefined: merge,
601
- },
602
- ),
777
+ this.addPendingOperations(
778
+ this.patchCreator.createDiff(this.getSnapshot(), changes, {
779
+ mergeUnknownObjects: !replaceSubObjects,
780
+ defaultUndefined: merge,
781
+ }),
603
782
  );
604
783
  };
605
784
 
606
- /**
607
- * List methods
608
- */
609
-
610
- /**
611
- * Returns the referent value of an item in the list, used for
612
- * operations which act on items. if the item is an object,
613
- * it will attempt to create an OID reference to it. If it
614
- * is a primitive, it will return the primitive.
615
- */
616
- private getItemRefValue = (item: ListItemValue<KeyValue>) => {
617
- if (typeof item === 'object') {
618
- const itemOid = maybeGetOid(item);
619
- if (!itemOid || !this.cache.hasOid(itemOid)) {
620
- throw new Error(
621
- `Cannot move object ${JSON.stringify(
622
- item,
623
- )} which does not exist in this list`,
624
- );
625
- }
626
- return itemOid;
627
- } else {
628
- return item;
629
- }
630
- };
631
-
632
- get length() {
633
- return this.value.length;
785
+ // array entity methods
786
+ get length(): number {
787
+ return this.view.length;
634
788
  }
635
789
 
636
- push = (value: ListItemInit<Init>) => {
637
- this.addPatches(
638
- this.store.patchCreator.createListPush(
790
+ push = (value: ListItemInit<Init>): void => {
791
+ this.addPendingOperations(
792
+ this.patchCreator.createListPush(
639
793
  this.oid,
640
- this.processInputValue(value, this.value.length),
794
+ this.processInputValue(value, this.view.length),
641
795
  ),
642
796
  );
643
797
  };
644
- insert = (index: number, value: ListItemInit<Init>) => {
645
- this.addPatches(
646
- this.store.patchCreator.createListInsert(
798
+
799
+ insert = (index: number, value: ListItemInit<Init>): void => {
800
+ this.addPendingOperations(
801
+ this.patchCreator.createListInsert(
647
802
  this.oid,
648
803
  index,
649
804
  this.processInputValue(value, index),
650
805
  ),
651
806
  );
652
807
  };
653
- move = (from: number, to: number) => {
654
- this.addPatches(
655
- this.store.patchCreator.createListMoveByIndex(this.oid, from, to),
808
+
809
+ move = (from: number, to: number): void => {
810
+ this.addPendingOperations(
811
+ this.patchCreator.createListMoveByIndex(this.oid, from, to),
656
812
  );
657
813
  };
658
- moveItem = (item: ListItemValue<KeyValue>, to: number) => {
814
+
815
+ moveItem = (item: ListItemValue<KeyValue>, to: number): void => {
659
816
  const itemRef = this.getItemRefValue(item);
660
- if (isObjectRef(itemRef)) {
661
- this.addPatches(
662
- this.store.patchCreator.createListMoveByRef(this.oid, itemRef, to),
817
+ if (isRef(itemRef)) {
818
+ this.addPendingOperations(
819
+ this.patchCreator.createListMoveByRef(this.oid, itemRef, to),
663
820
  );
664
821
  } else {
665
- const index = this.value.indexOf(itemRef);
666
- this.addPatches(
667
- this.store.patchCreator.createListMoveByIndex(this.oid, index, to),
668
- );
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);
669
831
  }
670
832
  };
671
- removeAll = (item: ListItemValue<KeyValue>) => {
672
- this.addPatches(
673
- this.store.patchCreator.createListRemove(
833
+
834
+ add = (value: ListItemValue<KeyValue>): void => {
835
+ this.addPendingOperations(
836
+ this.patchCreator.createListAdd(
674
837
  this.oid,
675
- this.getItemRefValue(item),
838
+ this.processInputValue(value, this.view.length),
676
839
  ),
677
840
  );
678
841
  };
679
- removeFirst = (item: ListItemValue<KeyValue>) => {
680
- this.addPatches(
681
- this.store.patchCreator.createListRemove(
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(
682
852
  this.oid,
683
853
  this.getItemRefValue(item),
684
854
  'first',
685
855
  ),
686
856
  );
687
857
  };
688
- removeLast = (item: ListItemValue<KeyValue>) => {
689
- this.addPatches(
690
- this.store.patchCreator.createListRemove(
858
+
859
+ removeLast = (item: ListItemValue<KeyValue>): void => {
860
+ this.addPendingOperations(
861
+ this.patchCreator.createListRemove(
691
862
  this.oid,
692
863
  this.getItemRefValue(item),
693
864
  'last',
694
865
  ),
695
866
  );
696
867
  };
697
- add = (item: ListItemValue<KeyValue>) => {
698
- this.addPatches(
699
- this.store.patchCreator.createListAdd(
700
- this.oid,
701
- this.processInputValue(item, this.value.length),
702
- ),
703
- );
704
- };
705
- has = (item: ListItemValue<KeyValue>) => {
706
- if (typeof item === 'object') {
707
- return this.value.some((val: unknown) => {
708
- if (isObjectRef(val)) return val.id === maybeGetOid(item);
709
- // Sets of files don't work right now, there's no way to compare them
710
- // effectively.
711
- if (isFileRef(val)) return false;
712
- return false;
713
- });
714
- }
715
- return this.value.includes(item);
716
- };
717
868
 
718
869
  // list implements an iterator which maps items to wrapped
719
870
  // versions
720
871
  [Symbol.iterator]() {
721
872
  let index = 0;
873
+ let length = this.view?.length;
722
874
  return {
723
875
  next: () => {
724
- if (index < this.value.length) {
876
+ if (index < length) {
725
877
  return {
726
878
  value: this.get(index++) as ListItemValue<KeyValue>,
727
879
  done: false,
@@ -735,140 +887,68 @@ export class Entity<
735
887
  };
736
888
  }
737
889
 
738
- // additional access methods
739
-
740
- private getAsWrapped = (): ListItemValue<KeyValue>[] => {
741
- if (!this.isList) throw new Error('Cannot map items of a non-list');
742
- return this.value.map(this.wrapValue);
743
- };
744
-
745
- map = <U>(callback: (value: ListItemValue<KeyValue>, index: number) => U) => {
746
- return this.getAsWrapped().map(callback);
890
+ map = <U>(
891
+ callback: (value: ListItemValue<KeyValue>, index: number) => U,
892
+ ): U[] => {
893
+ return this.view.map(callback);
747
894
  };
748
895
 
749
896
  filter = (
750
897
  callback: (value: ListItemValue<KeyValue>, index: number) => boolean,
751
- ) => {
752
- return this.getAsWrapped().filter((val, index) => {
753
- return callback(val, index);
754
- });
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
+ }
755
916
  };
756
917
 
757
918
  forEach = (
758
919
  callback: (value: ListItemValue<KeyValue>, index: number) => void,
759
- ) => {
760
- this.getAsWrapped().forEach(callback);
920
+ ): void => {
921
+ this.view.forEach(callback);
761
922
  };
762
923
 
763
- some = (predicate: (value: ListItemValue<KeyValue>) => boolean) => {
764
- return this.getAsWrapped().some(predicate);
924
+ some = (predicate: (value: ListItemValue<KeyValue>) => boolean): boolean => {
925
+ return this.view.some(predicate);
765
926
  };
766
927
 
767
- every = (predicate: (value: ListItemValue<KeyValue>) => boolean) => {
768
- return this.getAsWrapped().every(predicate);
928
+ every = (predicate: (value: ListItemValue<KeyValue>) => boolean): boolean => {
929
+ return this.view.every(predicate);
769
930
  };
770
931
 
771
- find = (predicate: (value: ListItemValue<KeyValue>) => boolean) => {
772
- return this.getAsWrapped().find(predicate);
932
+ find = (
933
+ predicate: (value: ListItemValue<KeyValue>) => boolean,
934
+ ): ListItemValue<KeyValue> | undefined => {
935
+ return this.view.find(predicate);
773
936
  };
774
937
 
775
- includes = (item: ListItemValue<KeyValue>) => {
776
- return this.has(item);
777
- };
778
- }
938
+ includes = this.has;
779
939
 
780
- export interface BaseEntity<
781
- Init,
782
- Value extends BaseEntityValue,
783
- Snapshot = DataFromInit<Init>,
784
- > {
785
- dispose: () => void;
786
- subscribe<EventName extends keyof EntityEvents>(
787
- event: EventName,
788
- callback: EntityEvents[EventName],
789
- ): () => void;
790
- get<Key extends keyof Value>(key: Key): Value[Key];
791
- getAll(): Value;
792
- getSnapshot(): Snapshot;
793
- readonly deleted: boolean;
794
- readonly hasSubscribers: boolean;
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();
795
945
  }
796
946
 
797
- type DeepPartial<T> = {
798
- [P in keyof T]?: T[P] extends Array<infer U>
799
- ? Array<DeepPartial<U>>
800
- : T[P] extends ReadonlyArray<infer U>
801
- ? ReadonlyArray<DeepPartial<U>>
802
- : DeepPartial<T[P]>;
803
- };
804
-
805
- export interface ObjectEntity<
806
- Init,
807
- Value extends BaseEntityValue,
808
- Snapshot = DataFromInit<Init>,
809
- > extends BaseEntity<Init, Value, Snapshot> {
810
- keys(): string[];
811
- entries(): [string, Exclude<Value[keyof Value], undefined>][];
812
- values(): Exclude<Value[keyof Value], undefined>[];
813
- set<Key extends keyof Init>(key: Key, value: Init[Key]): void;
814
- delete(key: DeletableKeys<Value>): void;
815
- update(
816
- value: DeepPartial<Init>,
817
- options?: { replaceSubObjects?: boolean; merge?: boolean },
818
- ): void;
819
- readonly isList: false;
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");
820
949
  }
821
950
 
822
- export interface ListEntity<
823
- Init,
824
- Value extends BaseEntityValue,
825
- Snapshot = DataFromInit<Init>,
826
- > extends Iterable<ListItemValue<Value>>,
827
- BaseEntity<Init, Value, Snapshot> {
828
- readonly isList: true;
829
- readonly length: number;
830
- push(value: ListItemInit<Init>): void;
831
- insert(index: number, value: ListItemInit<Init>): void;
832
- move(from: number, to: number): void;
833
- moveItem(item: ListItemValue<Value>, to: number): void;
834
- /**
835
- * A Set operation which adds a value if an equivalent value is not already present.
836
- * Object values are never the same.
837
- */
838
- add(value: ListItemValue<Value>): void;
839
- removeAll(item: ListItemValue<Value>): void;
840
- removeFirst(item: ListItemValue<Value>): void;
841
- removeLast(item: ListItemValue<Value>): void;
842
- map<U>(callback: (value: ListItemValue<Value>, index: number) => U): U[];
843
- filter(
844
- callback: (value: ListItemValue<Value>, index: number) => boolean,
845
- ): ListItemValue<Value>[];
846
- delete(index: number): void;
847
- has(value: ListItemValue<Value>): boolean;
848
- forEach(callback: (value: ListItemValue<Value>, index: number) => void): void;
849
- some(predicate: (value: ListItemValue<Value>) => boolean): boolean;
850
- every(predicate: (value: ListItemValue<Value>) => boolean): boolean;
851
- find(
852
- predicate: (value: ListItemValue<Value>) => boolean,
853
- ): ListItemValue<Value> | undefined;
854
- includes(value: ListItemValue<Value>): boolean;
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');
855
954
  }
856
-
857
- export type AnyEntity<
858
- Init,
859
- KeyValue extends BaseEntityValue,
860
- Snapshot extends any,
861
- > =
862
- | ListEntity<Init, KeyValue, Snapshot>
863
- | ObjectEntity<Init, KeyValue, Snapshot>;
864
-
865
- type ListItemValue<KeyValue> = KeyValue extends Array<infer T> ? T : never;
866
- type ListItemInit<Init> = Init extends Array<infer T> ? T : never;
867
-
868
- export type EntityDestructured<T extends AnyEntity<any, any, any> | null> =
869
- | (T extends ListEntity<any, infer KeyValue, any>
870
- ? KeyValue
871
- : T extends ObjectEntity<any, infer KeyValue, any>
872
- ? KeyValue
873
- : never)
874
- | (T extends null ? null : never);