@verdant-web/store 3.0.0-next.0 → 3.0.1

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