@dxos/echo-db 2.28.6-dev.ca9d0e33 → 2.28.7-dev.1dc18bee

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.
@@ -7,12 +7,12 @@ import debug from 'debug';
7
7
 
8
8
  import { Event } from '@dxos/async';
9
9
  import { failUndefined } from '@dxos/debug';
10
- import { DatabaseSnapshot, IEchoStream, ItemID, ItemSnapshot } from '@dxos/echo-protocol';
10
+ import { DatabaseSnapshot, IEchoStream, ItemID, ItemSnapshot, LinkSnapshot } from '@dxos/echo-protocol';
11
11
  import { createWritable } from '@dxos/feed-store';
12
12
  import { Model, ModelFactory, ModelMessage } from '@dxos/model-factory';
13
13
  import { jsonReplacer } from '@dxos/util';
14
14
 
15
- import { Entity, Item } from '../api';
15
+ import { Entity, Item, Link } from '../api';
16
16
  import { ItemManager, ModelConstructionOptions } from './item-manager';
17
17
 
18
18
  const log = debug('dxos:echo-db:item-demuxer');
@@ -43,6 +43,7 @@ export class ItemDemuxer {
43
43
  }
44
44
  });
45
45
 
46
+ // TODO(burdon): Factor out.
46
47
  // TODO(burdon): Should this implement some "back-pressure" (hints) to the PartyProcessor?
47
48
  return createWritable<IEchoStream>(async (message: IEchoStream) => {
48
49
  log('Reading:', JSON.stringify(message, jsonReplacer));
@@ -116,26 +117,40 @@ export class ItemDemuxer {
116
117
  createSnapshot (): DatabaseSnapshot {
117
118
  assert(this._options.snapshots, 'Snapshots are disabled');
118
119
  return {
119
- items: Array.from(this._itemManager.entities.values()).map(entity => this.createEntitySnapshot(entity))
120
+ items: this._itemManager.items.map(item => this.createItemSnapshot(item)),
121
+ links: this._itemManager.links.map(link => this.createLinkSnapshot(link))
120
122
  };
121
123
  }
122
124
 
123
- createEntitySnapshot (entity: Entity<Model<any>>): ItemSnapshot {
124
- const model = entity._stateManager.createSnapshot();
125
+ createItemSnapshot (item: Item<Model<any>>): ItemSnapshot {
126
+ const model = item._stateManager.createSnapshot();
125
127
 
126
128
  return {
127
- itemId: entity.id,
128
- itemType: entity.type,
129
- modelType: entity.modelMeta.type,
130
- parentId: (entity instanceof Item) ? entity.parent?.id : undefined,
129
+ itemId: item.id,
130
+ itemType: item.type,
131
+ modelType: item.modelMeta.type,
132
+ parentId: item.parent?.id,
133
+ model
134
+ };
135
+ }
136
+
137
+ createLinkSnapshot (link: Link<Model<any>>): LinkSnapshot {
138
+ const model = link._stateManager.createSnapshot();
139
+
140
+ return {
141
+ linkId: link.id,
142
+ linkType: link.type,
143
+ modelType: link.modelMeta.type,
144
+ source: link.source.id,
145
+ target: link.target.id,
131
146
  model
132
147
  };
133
148
  }
134
149
 
135
150
  async restoreFromSnapshot (snapshot: DatabaseSnapshot) {
136
- const items = snapshot.items ?? [];
137
- log(`Restoring ${items.length} items from snapshot.`);
151
+ const { items = [], links = [] } = snapshot;
138
152
 
153
+ log(`Restoring ${items.length} items from snapshot.`);
139
154
  for (const item of sortItemsTopologically(items)) {
140
155
  assert(item.itemId);
141
156
  assert(item.modelType);
@@ -149,9 +164,29 @@ export class ItemDemuxer {
149
164
  snapshot: item.model
150
165
  });
151
166
  }
167
+
168
+ log(`Restoring ${links.length} links from snapshot.`);
169
+ for (const link of links) {
170
+ assert(link.linkId);
171
+ assert(link.modelType);
172
+ assert(link.model);
173
+
174
+ await this._itemManager.constructLink({
175
+ itemId: link.linkId,
176
+ itemType: link.linkType,
177
+ modelType: link.modelType,
178
+ source: link.source,
179
+ target: link.target,
180
+ snapshot: link.model
181
+ });
182
+ }
152
183
  }
153
184
  }
154
185
 
186
+ /**
187
+ * Sort based on parents.
188
+ * @param items
189
+ */
155
190
  export function sortItemsTopologically (items: ItemSnapshot[]): ItemSnapshot[] {
156
191
  const snapshots: ItemSnapshot[] = [];
157
192
  const seenIds = new Set<ItemID>();
@@ -98,7 +98,7 @@ export class ItemManager {
98
98
  parentId?: ItemID,
99
99
  initProps?: any // TODO(burdon): Remove/change to array of mutations.
100
100
  ): Promise<Item<Model<unknown>>> {
101
- assert(this._writeStream);
101
+ assert(this._writeStream); // TODO(burdon): Throw ReadOnlyError();
102
102
  assert(modelType);
103
103
 
104
104
  if (!this._modelFactory.hasModel(modelType)) {
@@ -188,7 +188,8 @@ export class ItemManager {
188
188
  modelType, itemId, snapshot
189
189
  }: ModelConstructionOptions): Promise<StateManager<Model>> {
190
190
  // Convert model-specific outbound mutation to outbound envelope message.
191
- const outboundTransform = this._writeStream && mapFeedWriter<Uint8Array, EchoEnvelope>(mutation => ({ itemId, mutation }), this._writeStream);
191
+ const outboundTransform = this._writeStream && mapFeedWriter<Uint8Array, EchoEnvelope>(
192
+ mutation => ({ itemId, mutation }), this._writeStream);
192
193
 
193
194
  // Create the model with the outbound stream.
194
195
  return this._modelFactory.createModel<Model>(modelType, itemId, snapshot, this._memberKey, outboundTransform);
@@ -197,6 +198,7 @@ export class ItemManager {
197
198
  /**
198
199
  * Adds new entity to the tracked set. Sets up events and notifies any listeners waiting for this entity to be constructed.
199
200
  */
201
+ // TODO(burdon): Parent not used.
200
202
  private _addEntity (entity: Entity<any>, parent?: Item<any> | null) {
201
203
  assert(!this._entities.has(entity.id));
202
204
  this._entities.set(entity.id, entity);
@@ -217,19 +219,12 @@ export class ItemManager {
217
219
 
218
220
  /**
219
221
  * Constructs an item with the appropriate model.
220
- * @param itemId
221
- * @param modelType
222
- * @param itemType
223
- * @param readStream - Inbound mutation stream (from multiplexer).
224
- * @param [parentId] - ItemID of the parent of this Item (optional).
225
- * @param initialMutations
226
- * @param modelSnapshot
227
222
  */
228
223
  @timed(5_000)
229
224
  async constructItem ({
230
225
  itemId,
231
- modelType,
232
226
  itemType,
227
+ modelType,
233
228
  parentId,
234
229
  snapshot
235
230
  }: ItemConstructionOptions): Promise<Item<any>> {
@@ -259,14 +254,12 @@ export class ItemManager {
259
254
 
260
255
  /**
261
256
  * Constructs an item with the appropriate model.
262
- * @param readStream - Inbound mutation stream (from multiplexer).
263
- * @param parentId - ItemID of the parent of this Item (optional).
264
257
  */
265
258
  @timed(5_000)
266
259
  async constructLink ({
267
- itemId,
268
- modelType,
260
+ itemId, // TODO(burdon): linkId?
269
261
  itemType,
262
+ modelType,
270
263
  snapshot,
271
264
  source,
272
265
  target
@@ -29,7 +29,8 @@ describe('SnapshotStore', () => {
29
29
  items: [{
30
30
  itemId: createId(),
31
31
  itemType: 'example:test'
32
- }]
32
+ }],
33
+ links: []
33
34
  }
34
35
  };
35
36
 
@@ -8,7 +8,7 @@ import { it as test } from 'mocha';
8
8
 
9
9
  import { waitForCondition } from '@dxos/async';
10
10
  import { PublicKey } from '@dxos/crypto';
11
- import { schema, ItemID, PartyKey } from '@dxos/echo-protocol';
11
+ import { schema, ItemID, MockFeedWriter, PartyKey } from '@dxos/echo-protocol';
12
12
  import { ModelFactory } from '@dxos/model-factory';
13
13
  import { ObjectModel, ValueUtil } from '@dxos/object-model';
14
14
 
@@ -16,6 +16,7 @@ import { ItemDemuxer, ItemManager } from '../database';
16
16
  import { createTestInstance } from '../testing';
17
17
 
18
18
  const log = debug('dxos:snapshot:test');
19
+ debug.enable('dxos:echo-db:*');
19
20
 
20
21
  // TODO(burdon): Remove "foo", etc from tests.
21
22
  describe('snapshot', () => {
@@ -59,15 +60,28 @@ describe('snapshot', () => {
59
60
  test('produce & serialize a snapshot', async () => {
60
61
  const echo = await createTestInstance({ initialize: true });
61
62
  const party = await echo.createParty();
62
- const item = await party.database.createItem({ model: ObjectModel, props: { foo: 'foo' } });
63
- await item.model.setProperty('foo', 'bar');
63
+ const item1 = await party.database.createItem({ model: ObjectModel, props: { title: 'Item1 - Creation' } });
64
+ await item1.model.setProperty('title', 'Item1 - Modified');
65
+ const item2 = await party.database.createItem({ model: ObjectModel, props: { title: 'Item2 - Creation' } });
66
+ await item2.model.setProperty('title', 'Item2 - Modified');
67
+
68
+ const link = await party.database.createLink({ source: item1, target: item2 });
64
69
 
65
70
  const snapshot = party.createSnapshot();
66
- expect(snapshot.database?.items).toHaveLength(2);
67
- expect(snapshot.database?.items?.find(i => i.itemId === item.id)?.model?.snapshot).toBeDefined();
71
+ expect(snapshot.database?.items).toHaveLength(3); // 1 party + 2 items
72
+ expect(snapshot.database?.links).toHaveLength(1); // 1 link
73
+ expect(snapshot.database?.items?.find(i => i.itemId === item1.id)?.model?.snapshot).toBeDefined();
74
+ expect(snapshot.database?.items?.find(i => i.itemId === item2.id)?.model?.snapshot).toBeDefined();
75
+ expect(snapshot.database?.links?.find(l => l.linkId === link.id)?.linkId).toBeDefined();
76
+
77
+ // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
78
+ const modelSnapshot1 = ObjectModel.meta.snapshotCodec?.decode(snapshot.database?.items?.find(i => i.itemId === item1.id)?.model?.snapshot!);
79
+ expect(modelSnapshot1).toEqual({ root: ValueUtil.createMessage({ title: 'Item1 - Modified' }) });
80
+
68
81
  // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
69
- const modelSnapshot = ObjectModel.meta.snapshotCodec?.decode(snapshot.database?.items?.find(i => i.itemId === item.id)?.model?.snapshot!);
70
- expect(modelSnapshot).toEqual({ root: ValueUtil.createMessage({ foo: 'bar' }) });
82
+ const modelSnapshot2 = ObjectModel.meta.snapshotCodec?.decode(snapshot.database?.items?.find(i => i.itemId === item2.id)?.model?.snapshot!);
83
+ expect(modelSnapshot2).toEqual({ root: ValueUtil.createMessage({ title: 'Item2 - Modified' }) });
84
+
71
85
  expect(snapshot.halo?.messages && snapshot.halo?.messages?.length > 0).toBeTruthy();
72
86
  expect(snapshot.timeframe?.size()).toBe(1);
73
87
 
@@ -78,12 +92,68 @@ describe('snapshot', () => {
78
92
  expect(echo.isOpen).toBe(false);
79
93
  });
80
94
 
95
+ test('restore from snapshot', async () => {
96
+ const modelFactory = new ModelFactory().registerModel(ObjectModel);
97
+
98
+ let data;
99
+ {
100
+ const itemManager = new ItemManager(modelFactory, PublicKey.random(), new MockFeedWriter());
101
+ const itemDemuxer = new ItemDemuxer(itemManager, modelFactory, { snapshots: true });
102
+
103
+ await itemManager.constructItem({
104
+ itemId: 'item-1',
105
+ itemType: 'test-item-type',
106
+ modelType: ObjectModel.meta.type,
107
+ snapshot: {}
108
+ });
109
+
110
+ await itemManager.constructItem({
111
+ itemId: 'item-2',
112
+ itemType: 'test-item-type',
113
+ modelType: ObjectModel.meta.type,
114
+ snapshot: {}
115
+ });
116
+
117
+ await itemManager.constructLink({
118
+ itemId: 'link-1',
119
+ itemType: 'test-link-type',
120
+ modelType: ObjectModel.meta.type,
121
+ source: 'item-1',
122
+ target: 'item-2',
123
+ snapshot: {}
124
+ });
125
+
126
+ // Create snapshot.
127
+ console.log('encoding...');
128
+ const snapshot = itemDemuxer.createSnapshot();
129
+ data = schema.getCodecForType('dxos.echo.snapshot.DatabaseSnapshot').encode(snapshot);
130
+ }
131
+
132
+ {
133
+ const itemManager = new ItemManager(modelFactory, PublicKey.random());
134
+ const itemDemuxer = new ItemDemuxer(itemManager, modelFactory, { snapshots: true });
135
+
136
+ // Decode snapshot.
137
+ console.log('decoding...');
138
+ const snapshot = schema.getCodecForType('dxos.echo.snapshot.DatabaseSnapshot').decode(data);
139
+ await itemDemuxer.restoreFromSnapshot(snapshot);
140
+
141
+ expect(itemManager.items).toHaveLength(2);
142
+ expect(itemManager.links).toHaveLength(1);
143
+
144
+ const [item1, item2] = itemManager.items;
145
+ const [link] = itemManager.links;
146
+ expect(link.source.id).toBe(item1.id);
147
+ expect(link.target.id).toBe(item2.id);
148
+ }
149
+ });
150
+
81
151
  test('restore from empty snapshot', async () => {
82
152
  const modelFactory = new ModelFactory().registerModel(ObjectModel);
83
153
  const itemManager = new ItemManager(modelFactory, PublicKey.random());
84
154
  const itemDemuxer = new ItemDemuxer(itemManager, modelFactory);
85
155
 
86
- // TODO(burdon): Test.
156
+ // TODO(burdon): Do actual test.
87
157
  await itemDemuxer.restoreFromSnapshot({});
88
158
  expect(true).toBeTruthy();
89
159
  });