@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,256 @@
1
1
  "use strict";
2
- var _a, _b;
3
2
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.Entity = exports.refreshEntity = exports.DEEP_CHANGE = exports.REBASE = exports.DELETE = exports.ADD_OPERATIONS = void 0;
3
+ exports.Entity = void 0;
5
4
  const common_1 = require("@verdant-web/common");
6
5
  const utils_js_1 = require("../files/utils.js");
7
- exports.ADD_OPERATIONS = '@@addOperations';
8
- exports.DELETE = '@@delete';
9
- exports.REBASE = '@@rebase';
10
- const REFRESH = '@@refresh';
11
- exports.DEEP_CHANGE = '@@deepChange';
12
- function refreshEntity(entity, info) {
13
- return entity[REFRESH](info);
14
- }
15
- exports.refreshEntity = refreshEntity;
16
- class Entity {
17
- hasSubscribersToDeepChanges() {
18
- return this.events.subscriberCount('changeDeep') > 0;
19
- }
20
- get hasSubscribers() {
21
- var _c, _d;
22
- if (this.events.totalSubscriberCount() > 0) {
23
- return true;
24
- }
25
- // even if nobody subscribes directly to this entity, if a parent
26
- // has a deep subscription that counts.
27
- let parent = (_c = this.parent) === null || _c === void 0 ? void 0 : _c.deref();
28
- while (parent) {
29
- if (parent.hasSubscribersToDeepChanges()) {
30
- return true;
31
- }
32
- parent = (_d = parent.parent) === null || _d === void 0 ? void 0 : _d.deref();
33
- }
34
- return false;
35
- }
36
- get deleted() {
37
- return this._deleted;
38
- }
39
- get value() {
40
- return this._current;
41
- }
42
- get isList() {
43
- return Array.isArray(this._current);
44
- }
45
- get updatedAt() {
46
- return this._updatedAt;
47
- }
48
- get deepUpdatedAt() {
49
- if (this.cachedDeepUpdatedAt)
50
- return this.cachedDeepUpdatedAt;
51
- // iterate over all children and take the latest timestamp
52
- let latest = this._updatedAt;
53
- if (this.isList) {
54
- this.forEach((child) => {
55
- if (child instanceof Entity) {
56
- const childTimestamp = child.deepUpdatedAt;
57
- if (childTimestamp && (!latest || childTimestamp > latest)) {
58
- latest = childTimestamp;
59
- }
60
- }
61
- });
62
- }
63
- else {
64
- this.values().forEach((child) => {
65
- if (child instanceof Entity) {
66
- const childTimestamp = child.deepUpdatedAt;
67
- if (childTimestamp && (!latest || childTimestamp > latest)) {
68
- latest = childTimestamp;
69
- }
70
- }
71
- });
72
- }
73
- this.cachedDeepUpdatedAt = latest;
74
- return latest;
75
- }
76
- get uid() {
77
- return this.oid;
78
- }
79
- constructor({ oid, store, fieldSchema, cache, parent, onAllUnsubscribed, readonlyKeys = [], fieldPath = [], }) {
80
- // if current is null, the entity was deleted.
81
- this._current = null;
82
- this._deleted = false;
83
- this.cachedSnapshot = null;
84
- this.cachedDestructure = null;
6
+ const index_js_1 = require("../index.js");
7
+ const EntityCache_js_1 = require("./EntityCache.js");
8
+ class Entity extends common_1.EventSubscriber {
9
+ constructor({ oid, schema, entityFamily: childCache, parent, ctx, metadataFamily, readonlyKeys, files, patchCreator, events, }) {
10
+ super();
11
+ this.fieldPath = [];
12
+ // an internal representation of this Entity.
13
+ // if present, this is the cached, known value. If null,
14
+ // the entity is deleted. If undefined, we need to recompute
15
+ // the view.
16
+ this._viewData = undefined;
17
+ this.validationError = undefined;
85
18
  this.cachedDeepUpdatedAt = null;
86
- this._updatedAt = null;
87
- this[_a] = (info) => {
88
- const { view, deleted, lastTimestamp } = this.cache.computeView(this.oid);
89
- this._current = view;
90
- const restored = this._deleted && !deleted;
91
- this._deleted = deleted;
92
- this.cachedDestructure = null;
93
- this._updatedAt = lastTimestamp ? lastTimestamp : null;
94
- this.cachedDeepUpdatedAt = null;
95
- if (this._deleted) {
96
- this.events.emit('delete', info);
97
- }
98
- else {
99
- this.events.emit('change', info);
100
- this[exports.DEEP_CHANGE](this, info);
101
- }
102
- if (restored) {
103
- this.cachedSnapshot = null;
104
- this.events.emit('restore', info);
19
+ // only used for root entities to track delete/restore state.
20
+ this.wasDeletedLastChange = false;
21
+ this.cachedView = undefined;
22
+ this.onAdd = (_store, data) => {
23
+ if (data.oid === this.oid) {
24
+ this.addConfirmedData(data);
105
25
  }
106
26
  };
107
- this[_b] = (source, info) => {
108
- var _c;
109
- this.cachedSnapshot = null;
110
- this.cachedDeepUpdatedAt = null;
111
- this.events.emit('changeDeep', source, info);
112
- const parent = (_c = this.parent) === null || _c === void 0 ? void 0 : _c.deref();
113
- if (parent) {
114
- parent[exports.DEEP_CHANGE](source, info);
27
+ this.onReplace = (_store, data) => {
28
+ if (data.oid === this.oid) {
29
+ this.replaceAllData(data);
115
30
  }
116
31
  };
117
- this.getChildFieldSchema = (key) => {
118
- if (this.fieldSchema.type === 'object') {
119
- return this.fieldSchema.properties[key];
32
+ this.onResetAll = () => {
33
+ this.resetAllData();
34
+ };
35
+ this.childIsNull = (child) => {
36
+ if (child instanceof Entity) {
37
+ const childView = child.view;
38
+ return childView === null || childView === undefined;
120
39
  }
121
- else if (this.fieldSchema.type === 'array') {
122
- return this.fieldSchema.items;
40
+ return child === null || child === undefined;
41
+ };
42
+ /**
43
+ * Pruning - when entities have invalid children, we 'prune' that
44
+ * data up to the nearest prunable point - a nullable field,
45
+ * or a list.
46
+ */
47
+ this.validate = (0, common_1.memoByKeys)(() => {
48
+ var _a;
49
+ this.validationError =
50
+ (_a = (0, common_1.validateEntityField)({
51
+ field: this.schema,
52
+ value: this.rawView,
53
+ fieldPath: this.fieldPath,
54
+ depth: 1,
55
+ })) !== null && _a !== void 0 ? _a : undefined;
56
+ return this.validationError;
57
+ }, () => [this.viewData]);
58
+ this.viewWithMappedChildren = (mapper) => {
59
+ const view = this.view;
60
+ if (!view) {
61
+ return null;
123
62
  }
124
- else if (this.fieldSchema.type === 'map') {
125
- return this.fieldSchema.values;
63
+ if (Array.isArray(view)) {
64
+ const mapped = view.map((value) => {
65
+ if (value instanceof Entity || value instanceof index_js_1.EntityFile) {
66
+ return mapper(value);
67
+ }
68
+ else {
69
+ return value;
70
+ }
71
+ });
72
+ (0, common_1.assignOid)(mapped, this.oid);
73
+ return mapped;
126
74
  }
127
- else if (this.fieldSchema.type === 'any') {
128
- return this.fieldSchema;
75
+ else {
76
+ const mapped = Object.entries(view).reduce((acc, [key, value]) => {
77
+ if (value instanceof Entity || value instanceof index_js_1.EntityFile) {
78
+ acc[key] = mapper(value);
79
+ }
80
+ else {
81
+ acc[key] = value;
82
+ }
83
+ return acc;
84
+ }, {});
85
+ (0, common_1.assignOid)(mapped, this.oid);
86
+ return mapped;
129
87
  }
130
- throw new Error('Invalid field schema');
131
88
  };
132
- this.dispose = () => {
133
- this.events.dispose();
89
+ /**
90
+ * A current snapshot of this Entity's data, including nested
91
+ * Entities.
92
+ */
93
+ this.getSnapshot = () => {
94
+ return this.viewWithMappedChildren((child) => child.getSnapshot());
95
+ };
96
+ // change management methods (internal use only)
97
+ this.addPendingOperations = (operations) => {
98
+ this.ctx.log('debug', 'Entity: adding pending operations', this.oid);
99
+ const changes = this.metadataFamily.addPendingData(operations);
100
+ for (const change of changes) {
101
+ this.change(change);
102
+ }
134
103
  };
135
- this.subscribe = (event, callback) => {
136
- const unsubscribe = this.events.subscribe(event, callback);
137
- return unsubscribe;
104
+ this.addConfirmedData = (data) => {
105
+ this.ctx.log('debug', 'Entity: adding confirmed data', this.oid);
106
+ const changes = this.metadataFamily.addConfirmedData(data);
107
+ for (const change of changes) {
108
+ this.change(change);
109
+ }
138
110
  };
139
- this.addPatches = (patches) => {
140
- this.store.addLocalOperations(patches);
111
+ this.replaceAllData = (data) => {
112
+ this.ctx.log('debug', 'Entity: replacing all data', this.oid);
113
+ const changes = this.metadataFamily.replaceAllData(data);
114
+ for (const change of changes) {
115
+ this.change(change);
116
+ }
141
117
  };
142
- this.cloneCurrent = () => {
143
- if (this._current === undefined) {
144
- return undefined;
118
+ this.resetAllData = () => {
119
+ this.ctx.log('debug', 'Entity: resetting all data', this.oid);
120
+ this.cachedDeepUpdatedAt = null;
121
+ this.cachedView = undefined;
122
+ this._viewData = undefined;
123
+ const changes = this.metadataFamily.replaceAllData({});
124
+ for (const change of changes) {
125
+ this.change(change);
126
+ }
127
+ };
128
+ this.change = (ev) => {
129
+ if (ev.oid === this.oid) {
130
+ // reset cached view
131
+ this._viewData = undefined;
132
+ this.cachedView = undefined;
133
+ // chain deepChanges to parents
134
+ this.deepChange(this, ev);
135
+ // emit the change, it's for us
136
+ this.ctx.log('Emitting change event', this.oid);
137
+ this.emit('change', { isLocal: ev.isLocal });
138
+ // for root entities, we need to go ahead and decide if we're
139
+ // deleted or not - so queries can exclude us if we are.
140
+ if (!this.parent) {
141
+ // newly deleted - emit event
142
+ if (this.deleted && !this.wasDeletedLastChange) {
143
+ this.ctx.log('debug', 'Entity deleted', this.oid);
144
+ this.emit('delete', { isLocal: ev.isLocal });
145
+ this.wasDeletedLastChange = true;
146
+ }
147
+ else if (!this.deleted && this.wasDeletedLastChange) {
148
+ this.ctx.log('debug', 'Entity restored', this.oid);
149
+ // newly restored - emit event
150
+ this.emit('restore', { isLocal: ev.isLocal });
151
+ this.wasDeletedLastChange = false;
152
+ }
153
+ }
154
+ }
155
+ else {
156
+ // forward it to the correct family member. if none exists
157
+ // in cache, no one will hear it anyways.
158
+ const other = this.entityFamily.getCached(ev.oid);
159
+ if (other && other instanceof Entity) {
160
+ other.change(ev);
161
+ }
145
162
  }
146
- return (0, common_1.cloneDeep)(this._current);
147
163
  };
148
- this.getSubObject = (oid, key) => {
149
- const fieldSchema = this.getChildFieldSchema(key);
150
- // this is a failure case, but trying to be graceful about it...
151
- // @ts-ignore
152
- // if (!fieldSchema) return null;
153
- return this.cache.getEntity({
164
+ this.deepChange = (target, ev) => {
165
+ var _a;
166
+ // reset cached deep updated at timestamp; either this
167
+ // entity or children have changed
168
+ this.cachedDeepUpdatedAt = null;
169
+ // reset this flag to recompute snapshot data - children
170
+ // or self has changed. new pruning needs to happen.
171
+ this.cachedView = undefined;
172
+ this.ctx.log('debug', 'Deep change detected at', this.oid, 'reset cached view');
173
+ this.ctx.log('debug', 'Emitting deep change event', this.oid);
174
+ this.emit('changeDeep', target, ev);
175
+ (_a = this.parent) === null || _a === void 0 ? void 0 : _a.deepChange(target, ev);
176
+ };
177
+ this.getChild = (key, oid) => {
178
+ const schema = (0, common_1.getChildFieldSchema)(this.schema, key);
179
+ if (!schema) {
180
+ throw new Error(`No schema for key ${String(key)} in ${JSON.stringify(this.schema)}`);
181
+ }
182
+ return this.entityFamily.get({
154
183
  oid,
155
- fieldSchema,
184
+ schema,
185
+ entityFamily: this.entityFamily,
186
+ metadataFamily: this.metadataFamily,
156
187
  parent: this,
157
- fieldKey: key,
188
+ ctx: this.ctx,
189
+ files: this.files,
190
+ fieldPath: [...this.fieldPath, key],
191
+ patchCreator: this.patchCreator,
192
+ events: this.events,
158
193
  });
159
194
  };
160
- this.wrapValue = (value, key) => {
161
- if ((0, common_1.isObjectRef)(value)) {
162
- const oid = value.id;
163
- const subObject = this.getSubObject(oid, key);
164
- if (subObject) {
165
- return subObject;
166
- }
167
- throw new Error(`CACHE MISS: Subobject ${oid} does not exist on ${this.oid}`);
168
- }
169
- else if ((0, common_1.isFileRef)(value)) {
170
- const file = this.store.getFile(value.id);
171
- if (file) {
195
+ // generic entity methods
196
+ /**
197
+ * Gets a value from this Entity. If the value
198
+ * is an object, it will be wrapped in another
199
+ * Entity.
200
+ */
201
+ this.get = (key) => {
202
+ assertNotSymbol(key);
203
+ const view = this.rawView;
204
+ if (!view) {
205
+ throw new Error(`Cannot access data at key ${key} on deleted entity ${this.oid}`);
206
+ }
207
+ const child = view[key];
208
+ const schema = (0, common_1.getChildFieldSchema)(this.schema, key);
209
+ if (!schema) {
210
+ throw new Error(`No schema for key ${String(key)} in ${JSON.stringify(this.schema)}`);
211
+ }
212
+ if ((0, common_1.isRef)(child)) {
213
+ if ((0, common_1.isFileRef)(child)) {
214
+ if (schema.type !== 'file') {
215
+ throw new Error(`Expected file schema for key ${String(key)}, got ${schema.type}`);
216
+ }
217
+ const file = this.files.get(child.id, {
218
+ downloadRemote: !!schema.downloadRemote,
219
+ });
220
+ // FIXME: this seems bad and inconsistent
172
221
  file.subscribe('change', () => {
173
- this[exports.DEEP_CHANGE](this, {
174
- isLocal: false,
175
- });
222
+ this.deepChange(this, { isLocal: false, oid: this.oid });
176
223
  });
177
224
  return file;
178
225
  }
226
+ else {
227
+ return this.getChild(key, child.id);
228
+ }
229
+ }
230
+ else {
231
+ // prune invalid primitive fields
232
+ if ((0, common_1.validateEntityField)({
233
+ field: schema,
234
+ value: child,
235
+ fieldPath: [...this.fieldPath, key],
236
+ depth: 1,
237
+ requireDefaults: true,
238
+ })) {
239
+ if ((0, common_1.hasDefault)(schema)) {
240
+ return (0, common_1.getDefault)(schema);
241
+ }
242
+ if ((0, common_1.isNullable)(schema)) {
243
+ return null;
244
+ }
245
+ return undefined;
246
+ }
247
+ return child;
179
248
  }
180
- return value;
181
249
  };
182
250
  this.processInputValue = (value, key) => {
251
+ if (this.readonlyKeys.includes(key)) {
252
+ throw new Error(`Cannot set readonly key ${key.toString()}`);
253
+ }
183
254
  // disassociate incoming OIDs on values and generally break object
184
255
  // references. cloning doesn't work on files so those are
185
256
  // filtered out.
@@ -194,324 +265,427 @@ class Entity {
194
265
  // referenced in multiple entities, which could mean introduction
195
266
  // of foreign OIDs, or one object being assigned different OIDs
196
267
  // with unexpected results.
197
- if (!(value instanceof File)) {
268
+ if (!(0, utils_js_1.isFile)(value)) {
198
269
  value = (0, common_1.cloneDeep)(value, false);
199
270
  }
200
- const fieldSchema = this.getChildFieldSchema(key);
271
+ const fieldSchema = (0, common_1.getChildFieldSchema)(this.schema, key);
201
272
  if (fieldSchema) {
202
273
  (0, common_1.traverseCollectionFieldsAndApplyDefaults)(value, fieldSchema);
274
+ const validationError = (0, common_1.validateEntityField)({
275
+ field: fieldSchema,
276
+ value,
277
+ fieldPath: [...this.fieldPath, key],
278
+ });
279
+ if (validationError) {
280
+ // TODO: is it a good idea to throw an error here? a runtime error won't be that helpful,
281
+ // but also we don't really want invalid data supplied.
282
+ throw new Error(validationError.message);
283
+ }
203
284
  }
204
- const validationError = (0, common_1.validateEntityField)(fieldSchema, value, [
205
- ...this.fieldPath,
206
- key,
207
- ]);
208
- if (validationError) {
209
- // TODO: is it a good idea to throw an error here? a runtime error won't be that helpful,
210
- // but also we don't really want invalid data supplied.
211
- throw new Error(validationError);
212
- }
213
- return (0, utils_js_1.processValueFiles)(value, this.store.addFile);
214
- };
215
- this.get = (key) => {
216
- if (this.value === undefined || this.value === null) {
217
- throw new Error('Cannot access deleted entity');
218
- }
219
- const value = this.value[key];
220
- return this.wrapValue(value, key);
285
+ return (0, utils_js_1.processValueFiles)(value, this.files.add);
221
286
  };
222
- this.getAll = () => {
223
- if (this.value === undefined || this.value === null) {
224
- throw new Error('Cannot access deleted entity');
287
+ this.getDeleteMode = (key) => {
288
+ if (this.readonlyKeys.includes(key)) {
289
+ return false;
225
290
  }
226
- if (this.cachedDestructure)
227
- return this.cachedDestructure;
228
- let result;
229
- if (Array.isArray(this.value)) {
230
- result = this.value.map((value, index) => this.wrapValue(value, index));
291
+ // any is always deletable, and map values
292
+ if (this.schema.type === 'any' || this.schema.type === 'map') {
293
+ return 'delete';
231
294
  }
232
- else {
233
- result = {};
234
- for (const key in this.value) {
235
- result[key] = this.get(key);
295
+ if (this.schema.type === 'object') {
296
+ const property = this.schema.properties[key];
297
+ if (!property) {
298
+ // huh, the property doesn't exist. it's ok to
299
+ // remove I suppose.
300
+ return 'delete';
236
301
  }
302
+ if (property.type === 'any')
303
+ return 'delete';
304
+ // map can't be nullable. should it be?
305
+ if (property.type === 'map')
306
+ return false;
307
+ if (property.nullable)
308
+ return 'null';
237
309
  }
238
- this.cachedDestructure = result;
239
- return result;
310
+ // no other types are deletable
311
+ return false;
240
312
  };
241
313
  /**
242
- * Returns a copy of the entity and all sub-objects as
243
- * a plain object or array.
314
+ * Returns the referent value of an item in the list, used for
315
+ * operations which act on items. if the item is an object,
316
+ * it will attempt to create an OID reference to it. If it
317
+ * is a primitive, it will return the primitive.
244
318
  */
245
- this.getSnapshot = () => {
246
- var _c;
247
- if (!this.value) {
248
- return null;
249
- }
250
- if (this.deleted) {
251
- return null;
319
+ this.getItemRefValue = (item) => {
320
+ if (item instanceof Entity) {
321
+ return (0, common_1.createRef)(item.oid);
252
322
  }
253
- if (this.cachedSnapshot) {
254
- return this.cachedSnapshot;
323
+ if (item instanceof index_js_1.EntityFile) {
324
+ return (0, common_1.createFileRef)(item.id);
255
325
  }
256
- let snapshot;
257
- if (Array.isArray(this.value)) {
258
- snapshot = this.value.map((item, idx) => {
259
- var _c;
260
- if ((0, common_1.isObjectRef)(item)) {
261
- return (_c = this.getSubObject(item.id, idx)) === null || _c === void 0 ? void 0 : _c.getSnapshot();
262
- }
263
- else if ((0, common_1.isFileRef)(item)) {
264
- return this.getFileSnapshot(item);
265
- }
266
- return item;
267
- });
326
+ if (typeof item === 'object') {
327
+ const itemOid = (0, common_1.maybeGetOid)(item);
328
+ if (!itemOid || !this.entityFamily.has(itemOid)) {
329
+ throw new Error(`Cannot move object ${JSON.stringify(item)} which does not exist in this list`);
330
+ }
331
+ return (0, common_1.createRef)(itemOid);
268
332
  }
269
333
  else {
270
- snapshot = Object.assign({}, this.value);
271
- for (const [key, value] of Object.entries(snapshot)) {
272
- if ((0, common_1.isObjectRef)(value)) {
273
- snapshot[key] = (_c = this.getSubObject(value.id, key)) === null || _c === void 0 ? void 0 : _c.getSnapshot();
274
- }
275
- else if ((0, common_1.isFileRef)(value)) {
276
- snapshot[key] = this.getFileSnapshot(value);
277
- }
278
- }
334
+ return item;
279
335
  }
280
- (0, common_1.assignOid)(snapshot, this.oid);
281
- this.cachedSnapshot = snapshot;
282
- return snapshot;
336
+ };
337
+ this.set = (key, value) => {
338
+ assertNotSymbol(key);
339
+ this.addPendingOperations(this.patchCreator.createSet(this.oid, key, this.processInputValue(value, key)));
283
340
  };
284
341
  /**
285
- * Object methods
342
+ * Returns a destructured version of this Entity, where child
343
+ * Entities are accessible at their respective keys.
286
344
  */
287
- this.keys = () => {
288
- return Object.keys(this.value || {});
289
- };
290
- this.entries = () => {
291
- return Object.entries(this.getAll());
292
- };
293
- this.values = () => {
294
- return Object.values(this.getAll());
295
- };
296
- this.set = (key, value) => {
297
- if (this.readonlyKeys.includes(key)) {
298
- throw new Error(`Cannot set readonly key ${key.toString()}`);
299
- }
300
- this.addPatches(this.store.patchCreator.createSet(this.oid, key, this.processInputValue(value, key)));
345
+ this.getAll = () => {
346
+ return this.view;
301
347
  };
302
348
  this.delete = (key) => {
303
- if (Array.isArray(this.value)) {
304
- this.addPatches(this.store.patchCreator.createListDelete(this.oid, key, 1));
349
+ if (this.isList) {
350
+ assertNumber(key);
351
+ this.addPendingOperations(this.patchCreator.createListDelete(this.oid, key));
305
352
  }
306
353
  else {
307
- // the key must be deletable - i.e. optional in the schema
354
+ // the key must be deletable - i.e. optional in the schema.
308
355
  const deleteMode = this.getDeleteMode(key);
309
356
  if (!deleteMode) {
310
- throw new Error(`Cannot delete key ${key} - the property is not marked as optional in the schema`);
357
+ throw new Error(`Cannot delete key ${key.toString()} - the property is not marked as optional in the schema.`);
311
358
  }
312
359
  if (deleteMode === 'delete') {
313
- this.addPatches(this.store.patchCreator.createRemove(this.oid, key));
360
+ this.addPendingOperations(this.patchCreator.createRemove(this.oid, key));
314
361
  }
315
362
  else {
316
- this.addPatches(this.store.patchCreator.createSet(this.oid, key, null));
363
+ this.addPendingOperations(this.patchCreator.createSet(this.oid, key, null));
317
364
  }
318
365
  }
319
366
  };
320
- this.getDeleteMode = (key) => {
321
- if (this.readonlyKeys.includes(key)) {
322
- return false;
323
- }
324
- // 'any' is always deletable, and map values can be removed completely
325
- if (this.fieldSchema.type === 'any' || this.fieldSchema.type === 'map') {
326
- return 'delete';
327
- }
328
- if (this.fieldSchema.type === 'object') {
329
- const property = this.fieldSchema.properties[key];
330
- if (!property) {
331
- // huh, trying to delete a field that isn't specified
332
- // in the schema. we should use 'delete' mode.
333
- return 'delete';
334
- }
335
- if (property.type === 'any')
336
- return 'delete';
337
- // map can't be nullable
338
- // TODO: should it be?
339
- if (property.type === 'map')
340
- return false;
341
- // nullable properties can only be set null
342
- if (property.nullable)
343
- return 'null';
344
- }
345
- // no other parent objects support deleting
346
- return false;
367
+ // object entity methods
368
+ this.keys = () => {
369
+ if (!this.view)
370
+ return [];
371
+ return Object.keys(this.view);
372
+ };
373
+ this.entries = () => {
374
+ if (!this.view)
375
+ return [];
376
+ return Object.entries(this.view);
377
+ };
378
+ this.values = () => {
379
+ if (!this.view)
380
+ return [];
381
+ return Object.values(this.view);
347
382
  };
348
- /** @deprecated - renamed to delete */
349
- this.remove = this.delete.bind(this);
350
- this.update = (value, { replaceSubObjects = false, merge = true, } = {
351
- replaceSubObjects: false,
352
- merge: true,
353
- }) => {
354
- if (!merge &&
355
- this.fieldSchema.type !== 'any' &&
356
- this.fieldSchema.type !== 'map') {
383
+ this.update = (data, { merge = true, replaceSubObjects = false, } = {}) => {
384
+ if (!merge && this.schema.type !== 'any' && this.schema.type !== 'map') {
357
385
  throw new Error('Cannot use .update without merge if the field has a strict schema type. merge: false is only available on "any" or "map" types.');
358
386
  }
359
- for (const [key, field] of Object.entries(value)) {
387
+ const changes = {};
388
+ (0, common_1.assignOid)(changes, this.oid);
389
+ for (const [key, field] of Object.entries(data)) {
360
390
  if (this.readonlyKeys.includes(key)) {
361
391
  throw new Error(`Cannot set readonly key ${key.toString()}`);
362
392
  }
363
- const fieldSchema = this.getChildFieldSchema(key);
393
+ const fieldSchema = (0, common_1.getChildFieldSchema)(this.schema, key);
364
394
  if (fieldSchema) {
365
395
  (0, common_1.traverseCollectionFieldsAndApplyDefaults)(field, fieldSchema);
366
396
  }
397
+ changes[key] = this.processInputValue(field, key);
367
398
  }
368
- const withoutFiles = (0, utils_js_1.processValueFiles)(value, this.store.addFile);
369
- this.addPatches(this.store.patchCreator.createDiff(this.getSnapshot(), (0, common_1.assignOid)(withoutFiles, this.oid), {
399
+ this.addPendingOperations(this.patchCreator.createDiff(this.getSnapshot(), changes, {
370
400
  mergeUnknownObjects: !replaceSubObjects,
371
401
  defaultUndefined: merge,
372
402
  }));
373
403
  };
374
- /**
375
- * List methods
376
- */
377
- /**
378
- * Returns the referent value of an item in the list, used for
379
- * operations which act on items. if the item is an object,
380
- * it will attempt to create an OID reference to it. If it
381
- * is a primitive, it will return the primitive.
382
- */
383
- this.getItemRefValue = (item) => {
384
- if (typeof item === 'object') {
385
- const itemOid = (0, common_1.maybeGetOid)(item);
386
- if (!itemOid || !this.cache.hasOid(itemOid)) {
387
- throw new Error(`Cannot move object ${JSON.stringify(item)} which does not exist in this list`);
388
- }
389
- return itemOid;
390
- }
391
- else {
392
- return item;
393
- }
394
- };
395
404
  this.push = (value) => {
396
- this.addPatches(this.store.patchCreator.createListPush(this.oid, this.processInputValue(value, this.value.length)));
405
+ this.addPendingOperations(this.patchCreator.createListPush(this.oid, this.processInputValue(value, this.view.length)));
397
406
  };
398
407
  this.insert = (index, value) => {
399
- this.addPatches(this.store.patchCreator.createListInsert(this.oid, index, this.processInputValue(value, index)));
408
+ this.addPendingOperations(this.patchCreator.createListInsert(this.oid, index, this.processInputValue(value, index)));
400
409
  };
401
410
  this.move = (from, to) => {
402
- this.addPatches(this.store.patchCreator.createListMoveByIndex(this.oid, from, to));
411
+ this.addPendingOperations(this.patchCreator.createListMoveByIndex(this.oid, from, to));
403
412
  };
404
413
  this.moveItem = (item, to) => {
405
414
  const itemRef = this.getItemRefValue(item);
406
- if ((0, common_1.isObjectRef)(itemRef)) {
407
- this.addPatches(this.store.patchCreator.createListMoveByRef(this.oid, itemRef, to));
415
+ if ((0, common_1.isRef)(itemRef)) {
416
+ this.addPendingOperations(this.patchCreator.createListMoveByRef(this.oid, itemRef, to));
408
417
  }
409
418
  else {
410
- const index = this.value.indexOf(itemRef);
411
- this.addPatches(this.store.patchCreator.createListMoveByIndex(this.oid, index, to));
419
+ const index = this.view.indexOf(item);
420
+ if (index === -1) {
421
+ throw new Error(`Cannot move item ${JSON.stringify(item)} which does not exist in this list`);
422
+ }
423
+ this.move(index, to);
412
424
  }
413
425
  };
426
+ this.add = (value) => {
427
+ this.addPendingOperations(this.patchCreator.createListAdd(this.oid, this.processInputValue(value, this.view.length)));
428
+ };
414
429
  this.removeAll = (item) => {
415
- this.addPatches(this.store.patchCreator.createListRemove(this.oid, this.getItemRefValue(item)));
430
+ this.addPendingOperations(this.patchCreator.createListRemove(this.oid, this.getItemRefValue(item)));
416
431
  };
417
432
  this.removeFirst = (item) => {
418
- this.addPatches(this.store.patchCreator.createListRemove(this.oid, this.getItemRefValue(item), 'first'));
433
+ this.addPendingOperations(this.patchCreator.createListRemove(this.oid, this.getItemRefValue(item), 'first'));
419
434
  };
420
435
  this.removeLast = (item) => {
421
- this.addPatches(this.store.patchCreator.createListRemove(this.oid, this.getItemRefValue(item), 'last'));
422
- };
423
- this.add = (item) => {
424
- this.addPatches(this.store.patchCreator.createListAdd(this.oid, this.processInputValue(item, this.value.length)));
425
- };
426
- this.has = (item) => {
427
- if (typeof item === 'object') {
428
- return this.value.some((val) => {
429
- if ((0, common_1.isObjectRef)(val))
430
- return val.id === (0, common_1.maybeGetOid)(item);
431
- // Sets of files don't work right now, there's no way to compare them
432
- // effectively.
433
- if ((0, common_1.isFileRef)(val))
434
- return false;
435
- return false;
436
- });
437
- }
438
- return this.value.includes(item);
439
- };
440
- // additional access methods
441
- this.getAsWrapped = () => {
442
- if (!this.isList)
443
- throw new Error('Cannot map items of a non-list');
444
- return this.value.map(this.wrapValue);
436
+ this.addPendingOperations(this.patchCreator.createListRemove(this.oid, this.getItemRefValue(item), 'last'));
445
437
  };
446
438
  this.map = (callback) => {
447
- return this.getAsWrapped().map(callback);
439
+ return this.view.map(callback);
448
440
  };
449
441
  this.filter = (callback) => {
450
- return this.getAsWrapped().filter((val, index) => {
451
- return callback(val, index);
452
- });
442
+ return this.view.filter(callback);
443
+ };
444
+ this.has = (value) => {
445
+ if (!this.isList) {
446
+ throw new Error('has() is only available on list entities');
447
+ }
448
+ const itemRef = this.getItemRefValue(value);
449
+ if ((0, common_1.isRef)(itemRef)) {
450
+ return this.view.some((item) => {
451
+ if ((0, common_1.isRef)(item)) {
452
+ return (0, common_1.compareRefs)(item, itemRef);
453
+ }
454
+ });
455
+ }
456
+ else {
457
+ return this.view.includes(value);
458
+ }
453
459
  };
454
460
  this.forEach = (callback) => {
455
- this.getAsWrapped().forEach(callback);
461
+ this.view.forEach(callback);
456
462
  };
457
463
  this.some = (predicate) => {
458
- return this.getAsWrapped().some(predicate);
464
+ return this.view.some(predicate);
459
465
  };
460
466
  this.every = (predicate) => {
461
- return this.getAsWrapped().every(predicate);
467
+ return this.view.every(predicate);
462
468
  };
463
469
  this.find = (predicate) => {
464
- return this.getAsWrapped().find(predicate);
470
+ return this.view.find(predicate);
465
471
  };
466
- this.includes = (item) => {
467
- return this.has(item);
472
+ this.includes = this.has;
473
+ // TODO: make these escape hatches unnecessary
474
+ this.__getViewData__ = (oid, type) => {
475
+ return this.metadataFamily.get(oid).computeView(type === 'confirmed');
468
476
  };
477
+ this.__getFamilyOids__ = () => this.metadataFamily.getAllOids();
478
+ (0, common_1.assert)(!!oid, 'oid is required');
469
479
  this.oid = oid;
470
- const { collection } = (0, common_1.decomposeOid)(oid);
471
- this.collection = collection;
472
- this.store = store;
473
- this.fieldSchema = fieldSchema;
474
- this.fieldPath = fieldPath;
475
- this.readonlyKeys = readonlyKeys;
476
- this.cache = cache;
477
- this.parent = parent && this.cache.weakRef(parent);
478
- const { view, deleted, lastTimestamp } = this.cache.computeView(oid);
479
- this._current = view;
480
- this._deleted = deleted;
481
- this._updatedAt = lastTimestamp ? lastTimestamp : null;
482
- this.cachedDeepUpdatedAt = null;
483
- this.events = new common_1.EventSubscriber(() => {
484
- if (!this.hasSubscribers) {
485
- onAllUnsubscribed === null || onAllUnsubscribed === void 0 ? void 0 : onAllUnsubscribed();
486
- }
487
- });
488
- if (this.oid.includes('.') && !this.parent) {
489
- throw new Error('Parent must be provided for sub entities');
480
+ this.readonlyKeys = readonlyKeys || [];
481
+ this.ctx = ctx;
482
+ this.files = files;
483
+ this.schema = schema;
484
+ this.entityFamily =
485
+ childCache ||
486
+ new EntityCache_js_1.EntityCache({
487
+ initial: [this],
488
+ });
489
+ this.patchCreator = patchCreator;
490
+ this.metadataFamily = metadataFamily;
491
+ this.events = events;
492
+ this.parent = parent;
493
+ // TODO: should any but the root entity be listening to these?
494
+ if (!this.parent) {
495
+ events.add.attach(this.onAdd);
496
+ events.replace.attach(this.onReplace);
497
+ events.resetAll.attach(this.onResetAll);
490
498
  }
491
- (0, common_1.assert)(!!fieldSchema, 'Field schema must be provided');
492
499
  }
493
- getFileSnapshot(item) {
494
- const file = this.store.getFile(item.id);
495
- if (file.url) {
496
- return { id: item.id, url: file.url };
500
+ get metadata() {
501
+ return this.metadataFamily.get(this.oid);
502
+ }
503
+ /**
504
+ * The view of this Entity, not including nested
505
+ * entities (that's the snapshot - see #getSnapshot())
506
+ *
507
+ * Nested entities are represented by refs.
508
+ */
509
+ get viewData() {
510
+ if (this._viewData === undefined) {
511
+ this._viewData = this.metadata.computeView();
512
+ this.validate();
497
513
  }
498
- else if (file.loading || file.failed) {
499
- return { id: item.id, url: undefined };
514
+ return this._viewData;
515
+ }
516
+ /** convenience getter for viewData.view */
517
+ get rawView() {
518
+ return this.viewData.view;
519
+ }
520
+ /**
521
+ * An Entity's View includes the rendering of its underlying data,
522
+ * connecting of children where refs were, and validation
523
+ * and pruning according to schema.
524
+ */
525
+ get view() {
526
+ if (this.cachedView !== undefined) {
527
+ return this.cachedView;
528
+ }
529
+ if (this.viewData.deleted) {
530
+ return null;
531
+ }
532
+ // can't use invalid data - but this should be bubbled up to
533
+ // a prune point
534
+ const rawView = this.rawView;
535
+ const viewIsWrongType = (!rawView && !(0, common_1.isNullable)(this.schema)) ||
536
+ (this.schema.type === 'array' && !Array.isArray(rawView)) ||
537
+ ((this.schema.type === 'object' || this.schema.type === 'map') &&
538
+ !(0, common_1.isObject)(rawView));
539
+ if (viewIsWrongType) {
540
+ // this will cover lists and maps, too.
541
+ if ((0, common_1.hasDefault)(this.schema)) {
542
+ return (0, common_1.getDefault)(this.schema);
543
+ }
544
+ // force null - invalid - will require parent prune
545
+ return null;
546
+ }
547
+ this.cachedView = this.isList ? [] : {};
548
+ (0, common_1.assignOid)(this.cachedView, this.oid);
549
+ if (Array.isArray(rawView)) {
550
+ const schema = (0, common_1.getChildFieldSchema)(this.schema, 0);
551
+ if (!schema) {
552
+ /**
553
+ * PRUNE - this is a prune point. we can't continue
554
+ * to render this data, so we'll just return [].
555
+ * This skips the loop.
556
+ */
557
+ this.ctx.log('error', 'No child field schema for list entity.', this.oid);
558
+ }
559
+ else {
560
+ for (let i = 0; i < rawView.length; i++) {
561
+ const child = this.get(i);
562
+ if (this.childIsNull(child) && !(0, common_1.isNullable)(schema)) {
563
+ this.ctx.log('error', 'Child missing in non-nullable field', this.oid, 'index:', i);
564
+ // this item will be pruned.
565
+ }
566
+ else {
567
+ this.cachedView.push(child);
568
+ }
569
+ }
570
+ }
571
+ }
572
+ else if ((0, common_1.isObject)(rawView)) {
573
+ // iterate over known properties in object-type entities;
574
+ // for maps, we just iterate over the keys.
575
+ const keys = this.schema.type === 'object'
576
+ ? Object.keys(this.schema.properties)
577
+ : Object.keys(rawView);
578
+ for (const key of keys) {
579
+ const schema = (0, common_1.getChildFieldSchema)(this.schema, key);
580
+ if (!schema) {
581
+ /**
582
+ * PRUNE - this is a prune point. we can't continue
583
+ * to render this data. If this is a map, it will be
584
+ * pruned empty. Otherwise, prune moves upward.
585
+ *
586
+ * This exits the loop.
587
+ */
588
+ this.ctx.log('error', 'No child field schema for object entity at key', key);
589
+ if (this.schema.type === 'map') {
590
+ // it's valid to prune here if it's a map
591
+ this.cachedView = {};
592
+ }
593
+ else {
594
+ // otherwise prune moves upward
595
+ this.cachedView = null;
596
+ }
597
+ break;
598
+ }
599
+ const child = this.get(key);
600
+ if (this.childIsNull(child) && !(0, common_1.isNullable)(schema)) {
601
+ this.ctx.log('error', 'Child entity is missing for non-nullable field', this.oid, 'key:', key);
602
+ /**
603
+ * PRUNE - this is a prune point. we can't continue
604
+ * to render this data. If this is a map, we can ignore
605
+ * this value. Otherwise we must prune upward.
606
+ * This exits the loop.
607
+ */
608
+ if (this.schema.type !== 'map') {
609
+ this.cachedView = null;
610
+ break;
611
+ }
612
+ }
613
+ else {
614
+ this.cachedView[key] = child;
615
+ }
616
+ }
617
+ }
618
+ return this.cachedView;
619
+ }
620
+ get uid() {
621
+ return this.oid;
622
+ }
623
+ get deleted() {
624
+ return this.viewData.deleted || this.view === null;
625
+ }
626
+ get invalid() {
627
+ return !!this.validate();
628
+ }
629
+ get isList() {
630
+ // have to turn TS off here as our two interfaces both implement
631
+ // const values for this boolean.
632
+ return (this.schema.type === 'array' || Array.isArray(this.viewData.view));
633
+ }
634
+ get updatedAt() {
635
+ return this.viewData.updatedAt;
636
+ }
637
+ get deepUpdatedAt() {
638
+ if (this.cachedDeepUpdatedAt)
639
+ return this.cachedDeepUpdatedAt;
640
+ // iterate over all children and take the latest timestamp
641
+ let latest = this.updatedAt;
642
+ if (this.isList) {
643
+ this.forEach((child) => {
644
+ if (child instanceof Entity) {
645
+ const childTimestamp = child.deepUpdatedAt;
646
+ if (childTimestamp && (!latest || childTimestamp > latest)) {
647
+ latest = childTimestamp;
648
+ }
649
+ }
650
+ });
500
651
  }
501
652
  else {
502
- return { id: item.id, url: null };
653
+ this.values().forEach((child) => {
654
+ if (child instanceof Entity) {
655
+ const childTimestamp = child.deepUpdatedAt;
656
+ if (childTimestamp && (!latest || childTimestamp > latest)) {
657
+ latest = childTimestamp;
658
+ }
659
+ }
660
+ });
503
661
  }
662
+ this.cachedDeepUpdatedAt = latest;
663
+ return latest;
664
+ }
665
+ /**
666
+ * @internal - this is relevant to Verdant's system, not users.
667
+ *
668
+ * Indicates whether this document is from an outdated version
669
+ * of the schema - which means it cannot be used until it is upgraded.
670
+ */
671
+ get isOutdatedVersion() {
672
+ if (this.parent)
673
+ return this.parent.isOutdatedVersion;
674
+ return this.viewData.fromOlderVersion;
504
675
  }
676
+ // array entity methods
505
677
  get length() {
506
- return this.value.length;
678
+ return this.view.length;
507
679
  }
508
680
  // list implements an iterator which maps items to wrapped
509
681
  // versions
510
- [(_a = REFRESH, _b = exports.DEEP_CHANGE, Symbol.iterator)]() {
682
+ [Symbol.iterator]() {
683
+ var _a;
511
684
  let index = 0;
685
+ let length = (_a = this.view) === null || _a === void 0 ? void 0 : _a.length;
512
686
  return {
513
687
  next: () => {
514
- if (index < this.value.length) {
688
+ if (index < length) {
515
689
  return {
516
690
  value: this.get(index++),
517
691
  done: false,
@@ -526,4 +700,12 @@ class Entity {
526
700
  }
527
701
  }
528
702
  exports.Entity = Entity;
703
+ function assertNotSymbol(key) {
704
+ if (typeof key === 'symbol')
705
+ throw new Error("Symbol keys aren't supported");
706
+ }
707
+ function assertNumber(key) {
708
+ if (typeof key !== 'number')
709
+ throw new Error('Only number keys are supported in list entities');
710
+ }
529
711
  //# sourceMappingURL=Entity.js.map