@dxos/client-services 0.5.9-main.b8d8fee → 0.5.9-main.bd9c8b3

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 (39) hide show
  1. package/dist/lib/browser/{chunk-2EFXBSRZ.mjs → chunk-KI7FY3ZO.mjs} +718 -555
  2. package/dist/lib/browser/chunk-KI7FY3ZO.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1 -3
  4. package/dist/lib/browser/index.mjs.map +1 -1
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/packlets/testing/index.mjs +2 -3
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +2 -2
  8. package/dist/lib/node/{chunk-2SS7JAIR.cjs → chunk-XWMOEZYI.cjs} +869 -709
  9. package/dist/lib/node/chunk-XWMOEZYI.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +41 -43
  11. package/dist/lib/node/index.cjs.map +1 -1
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/packlets/testing/index.cjs +9 -10
  14. package/dist/lib/node/packlets/testing/index.cjs.map +2 -2
  15. package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
  16. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  17. package/dist/types/src/packlets/spaces/automerge-space-state.d.ts +4 -1
  18. package/dist/types/src/packlets/spaces/automerge-space-state.d.ts.map +1 -1
  19. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  20. package/dist/types/src/packlets/spaces/data-space.d.ts +9 -9
  21. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  22. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +23 -0
  23. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -0
  24. package/dist/types/src/packlets/spaces/spaces-service.d.ts +2 -2
  25. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  26. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  27. package/dist/types/src/version.d.ts +1 -1
  28. package/package.json +36 -36
  29. package/src/packlets/identity/identity-service.ts +33 -7
  30. package/src/packlets/services/service-context.ts +1 -4
  31. package/src/packlets/spaces/automerge-space-state.ts +11 -2
  32. package/src/packlets/spaces/data-space-manager.ts +35 -14
  33. package/src/packlets/spaces/data-space.ts +76 -148
  34. package/src/packlets/spaces/epoch-migrations.ts +135 -0
  35. package/src/packlets/spaces/spaces-service.ts +16 -4
  36. package/src/packlets/testing/test-builder.ts +1 -4
  37. package/src/version.ts +1 -1
  38. package/dist/lib/browser/chunk-2EFXBSRZ.mjs.map +0 -7
  39. package/dist/lib/node/chunk-2SS7JAIR.cjs.map +0 -7
@@ -3,10 +3,11 @@
3
3
  //
4
4
 
5
5
  import { Event } from '@dxos/async';
6
+ import { Resource, type Context } from '@dxos/context';
6
7
  import { type CredentialProcessor, type SpecificCredential, checkCredentialType } from '@dxos/credentials';
7
8
  import { type Credential, type Epoch } from '@dxos/protocols/proto/dxos/halo/credentials';
8
9
 
9
- export class AutomergeSpaceState implements CredentialProcessor {
10
+ export class AutomergeSpaceState extends Resource implements CredentialProcessor {
10
11
  public rootUrl: string | undefined = undefined;
11
12
  public lastEpoch: SpecificCredential<Epoch> | undefined = undefined;
12
13
 
@@ -14,7 +15,15 @@ export class AutomergeSpaceState implements CredentialProcessor {
14
15
 
15
16
  private _isProcessingRootDocs = false;
16
17
 
17
- constructor(private readonly _onNewRoot: (rootUrl: string) => void) {}
18
+ constructor(private readonly _onNewRoot: (rootUrl: string) => void) {
19
+ super();
20
+ }
21
+
22
+ protected override async _open(ctx: Context): Promise<void> {}
23
+
24
+ protected override async _close(ctx: Context): Promise<void> {
25
+ this._isProcessingRootDocs = false;
26
+ }
18
27
 
19
28
  async processCredential(credential: Credential) {
20
29
  if (!checkCredentialType(credential, 'dxos.halo.credentials.Epoch')) {
@@ -4,16 +4,16 @@
4
4
 
5
5
  import { Event, synchronized, trackLeaks } from '@dxos/async';
6
6
  import { type Doc } from '@dxos/automerge/automerge';
7
- import { type DocHandle, type AutomergeUrl } from '@dxos/automerge/automerge-repo';
7
+ import { type AutomergeUrl, type DocHandle } from '@dxos/automerge/automerge-repo';
8
8
  import { PropertiesType } from '@dxos/client-protocol';
9
- import { cancelWithContext, Context } from '@dxos/context';
9
+ import { Context, cancelWithContext } from '@dxos/context';
10
10
  import {
11
+ getCredentialAssertion,
11
12
  type CredentialSigner,
12
13
  type DelegateInvitationCredential,
13
- getCredentialAssertion,
14
14
  type MemberInfo,
15
15
  } from '@dxos/credentials';
16
- import { type EchoHost } from '@dxos/echo-db';
16
+ import { convertLegacyReferences, findInlineObjectOfType, type EchoHost } from '@dxos/echo-db';
17
17
  import {
18
18
  AuthStatus,
19
19
  type MetadataStore,
@@ -22,8 +22,14 @@ import {
22
22
  type SpaceProtocol,
23
23
  type SpaceProtocolSession,
24
24
  } from '@dxos/echo-pipeline';
25
- import { encodeReference, type ObjectStructure, type SpaceDoc } from '@dxos/echo-protocol';
26
- import { getTypeReference } from '@dxos/echo-schema';
25
+ import {
26
+ LEGACY_TYPE_PROPERTIES,
27
+ SpaceDocVersion,
28
+ encodeReference,
29
+ type ObjectStructure,
30
+ type SpaceDoc,
31
+ } from '@dxos/echo-protocol';
32
+ import { TYPE_PROPERTIES, generateEchoId, getTypeReference } from '@dxos/echo-schema';
27
33
  import { type FeedStore } from '@dxos/feed-store';
28
34
  import { invariant } from '@dxos/invariant';
29
35
  import { type Keyring } from '@dxos/keyring';
@@ -33,15 +39,15 @@ import { trace as Trace } from '@dxos/protocols';
33
39
  import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
34
40
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
35
41
  import { type SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
36
- import { type Credential, type ProfileDocument, SpaceMember } from '@dxos/protocols/proto/dxos/halo/credentials';
42
+ import { SpaceMember, type Credential, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
37
43
  import { type DelegateSpaceInvitation } from '@dxos/protocols/proto/dxos/halo/invitations';
38
44
  import { type PeerState } from '@dxos/protocols/proto/dxos/mesh/presence';
39
45
  import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
40
46
  import { type Timeframe } from '@dxos/timeframe';
41
47
  import { trace } from '@dxos/tracing';
42
- import { assignDeep, ComplexMap, deferFunction, forEachAsync } from '@dxos/util';
48
+ import { ComplexMap, assignDeep, deferFunction, forEachAsync } from '@dxos/util';
43
49
 
44
- import { DataSpace, findPropertiesObject } from './data-space';
50
+ import { DataSpace } from './data-space';
45
51
  import { spaceGenesis } from './genesis';
46
52
  import { createAuthProvider } from '../identity';
47
53
  import { type InvitationsManager } from '../invitations';
@@ -113,7 +119,7 @@ export class DataSpaceManager {
113
119
  const rootHandle = rootUrl ? this._echoHost.automergeRepo.find(rootUrl as AutomergeUrl) : undefined;
114
120
  const rootDoc = rootHandle?.docSync() as Doc<SpaceDoc> | undefined;
115
121
 
116
- const properties = rootDoc && findPropertiesObject(rootDoc);
122
+ const properties = rootDoc && findInlineObjectOfType(rootDoc, TYPE_PROPERTIES);
117
123
 
118
124
  return {
119
125
  key: space.key.toHex(),
@@ -204,9 +210,24 @@ export class DataSpaceManager {
204
210
  }
205
211
 
206
212
  async isDefaultSpace(space: DataSpace): Promise<boolean> {
207
- const rootDoc = await this._getSpaceRootDocument(space);
208
- const [_, properties] = findPropertiesObject(rootDoc.docSync()) ?? [];
209
- return properties?.data?.[DEFAULT_SPACE_KEY] === this._signingContext.identityKey.toHex();
213
+ if (!space.databaseRoot) {
214
+ return false;
215
+ }
216
+ switch (space.databaseRoot.getVersion()) {
217
+ case SpaceDocVersion.CURRENT: {
218
+ const [_, properties] = findInlineObjectOfType(space.databaseRoot.docSync()!, TYPE_PROPERTIES) ?? [];
219
+ return properties?.data?.[DEFAULT_SPACE_KEY] === this._signingContext.identityKey.toHex();
220
+ }
221
+ case SpaceDocVersion.LEGACY: {
222
+ const convertedDoc = await convertLegacyReferences(space.databaseRoot.docSync()!);
223
+ const [_, properties] = findInlineObjectOfType(convertedDoc, LEGACY_TYPE_PROPERTIES) ?? [];
224
+ return properties?.data?.[DEFAULT_SPACE_KEY] === this._signingContext.identityKey.toHex();
225
+ }
226
+
227
+ default:
228
+ log.warn('unknown space version', { version: space.databaseRoot.getVersion(), spaceId: space.id });
229
+ return false;
230
+ }
210
231
  }
211
232
 
212
233
  async createDefaultSpace() {
@@ -226,7 +247,7 @@ export class DataSpaceManager {
226
247
  },
227
248
  };
228
249
 
229
- const propertiesId = PublicKey.random().toHex();
250
+ const propertiesId = generateEchoId();
230
251
  document.change((doc: SpaceDoc) => {
231
252
  assignDeep(doc, ['objects', propertiesId], properties);
232
253
  });
@@ -2,27 +2,25 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { Event, asyncTimeout, scheduleTask, sleep, synchronized, trackLeaks } from '@dxos/async';
5
+ import { Event, Mutex, scheduleTask, sleep, synchronized, trackLeaks } from '@dxos/async';
6
6
  import { AUTH_TIMEOUT } from '@dxos/client-protocol';
7
7
  import { Context, ContextDisposedError, cancelWithContext } from '@dxos/context';
8
+ import type { SpecificCredential } from '@dxos/credentials';
8
9
  import { timed, warnAfterTimeout } from '@dxos/debug';
9
- import { type EchoHost } from '@dxos/echo-db';
10
- import {
11
- AutomergeDocumentLoaderImpl,
12
- createIdFromSpaceKey,
13
- createMappedFeedWriter,
14
- type MetadataStore,
15
- type Space,
16
- } from '@dxos/echo-pipeline';
17
- import { type ObjectStructure, type SpaceDoc } from '@dxos/echo-protocol';
18
- import { TYPE_PROPERTIES } from '@dxos/echo-schema';
10
+ import { type EchoHost, type DatabaseRoot } from '@dxos/echo-db';
11
+ import { createMappedFeedWriter, type MetadataStore, type Space } from '@dxos/echo-pipeline';
12
+ import { SpaceDocVersion } from '@dxos/echo-protocol';
19
13
  import { type FeedStore } from '@dxos/feed-store';
20
- import { failedInvariant, invariant } from '@dxos/invariant';
14
+ import { failedInvariant } from '@dxos/invariant';
21
15
  import { type Keyring } from '@dxos/keyring';
22
16
  import { PublicKey } from '@dxos/keys';
23
17
  import { log } from '@dxos/log';
24
18
  import { CancelledError, SystemError } from '@dxos/protocols';
25
- import { CreateEpochRequest, SpaceState, type Space as SpaceProto } from '@dxos/protocols/proto/dxos/client/services';
19
+ import {
20
+ type CreateEpochRequest,
21
+ SpaceState,
22
+ type Space as SpaceProto,
23
+ } from '@dxos/protocols/proto/dxos/client/services';
26
24
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
27
25
  import { type SpaceCache } from '@dxos/protocols/proto/dxos/echo/metadata';
28
26
  import {
@@ -36,10 +34,11 @@ import { type GossipMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/gos
36
34
  import { type Gossip, type Presence } from '@dxos/teleport-extension-gossip';
37
35
  import { Timeframe } from '@dxos/timeframe';
38
36
  import { trace } from '@dxos/tracing';
39
- import { ComplexSet, assignDeep } from '@dxos/util';
37
+ import { ComplexSet } from '@dxos/util';
40
38
 
41
39
  import { AutomergeSpaceState } from './automerge-space-state';
42
40
  import { type SigningContext } from './data-space-manager';
41
+ import { runEpochMigration } from './epoch-migrations';
43
42
  import { NotarizationPlugin } from './notarization-plugin';
44
43
  import { TrustedKeySetAuthVerifier } from '../identity';
45
44
 
@@ -100,8 +99,12 @@ export class DataSpace {
100
99
  // TODO(dmaretskyi): Move into Space?
101
100
  private readonly _automergeSpaceState = new AutomergeSpaceState((rootUrl) => this._onNewAutomergeRoot(rootUrl));
102
101
 
102
+ private readonly _epochProcessingMutex = new Mutex();
103
+
103
104
  private _state = SpaceState.CLOSED;
104
105
 
106
+ private _databaseRoot: DatabaseRoot | null = null;
107
+
105
108
  /**
106
109
  * Error for _state === SpaceState.ERROR.
107
110
  */
@@ -183,6 +186,10 @@ export class DataSpace {
183
186
  return this._automergeSpaceState;
184
187
  }
185
188
 
189
+ get databaseRoot(): DatabaseRoot | null {
190
+ return this._databaseRoot;
191
+ }
192
+
186
193
  @trace.info({ depth: null })
187
194
  private get _automergeInfo() {
188
195
  return {
@@ -203,6 +210,7 @@ export class DataSpace {
203
210
  await this._gossip.open();
204
211
  await this._notarizationPlugin.open();
205
212
  await this._inner.spaceState.addCredentialProcessor(this._notarizationPlugin);
213
+ await this._automergeSpaceState.open();
206
214
  await this._inner.spaceState.addCredentialProcessor(this._automergeSpaceState);
207
215
  await this._inner.open(new Context());
208
216
  this._state = SpaceState.CONTROL_ONLY;
@@ -228,6 +236,7 @@ export class DataSpace {
228
236
 
229
237
  await this._inner.close();
230
238
  await this._inner.spaceState.removeCredentialProcessor(this._automergeSpaceState);
239
+ await this._automergeSpaceState.close();
231
240
  await this._inner.spaceState.removeCredentialProcessor(this._notarizationPlugin);
232
241
  await this._notarizationPlugin.close();
233
242
 
@@ -282,12 +291,15 @@ export class DataSpace {
282
291
  // Allow other tasks to run before loading the data pipeline.
283
292
  await sleep(1);
284
293
 
294
+ const ready = this.stateUpdate.waitForCondition(() => this._state === SpaceState.READY);
295
+
285
296
  this._automergeSpaceState.startProcessingRootDocs();
286
297
 
287
- // Wait for the first epoch.
288
- await cancelWithContext(this._ctx, this.automergeSpaceState.ensureEpochInitialized());
298
+ // TODO(dmaretskyi): Change so `initializeDataPipeline` doesn't wait for the space to be READY, but rather any state with a valid root.
299
+ await ready;
300
+ }
289
301
 
290
- log('data pipeline ready');
302
+ private async _enterReadyState() {
291
303
  await this._callbacks.beforeReady?.();
292
304
 
293
305
  this._state = SpaceState.READY;
@@ -374,11 +386,10 @@ export class DataSpace {
374
386
 
375
387
  private _onNewAutomergeRoot(rootUrl: string) {
376
388
  log('loading automerge root doc for space', { space: this.key, rootUrl });
377
- // Override share policy = true for the root document.
378
- // Workaround for https://github.com/automerge/automerge-repo/pull/292
379
- this._echoHost.replicateDocument(rootUrl);
389
+
380
390
  const handle = this._echoHost.automergeRepo.find(rootUrl as any);
381
391
 
392
+ // TODO(dmaretskyi): Make this single-threaded (but doc loading should still be parallel to not block epoch processing).
382
393
  queueMicrotask(async () => {
383
394
  try {
384
395
  await warnAfterTimeout(5_000, 'Automerge root doc load timeout (DataSpace)', async () => {
@@ -388,6 +399,10 @@ export class DataSpace {
388
399
  return;
389
400
  }
390
401
 
402
+ // Ensure only one root is processed at a time.
403
+ using _guard = await this._epochProcessingMutex.acquire();
404
+
405
+ // Attaching space keys to legacy documents.
391
406
  const doc = handle.docSync() ?? failedInvariant();
392
407
  if (!doc.access?.spaceKey) {
393
408
  handle.change((doc: any) => {
@@ -397,10 +412,17 @@ export class DataSpace {
397
412
 
398
413
  // TODO(dmaretskyi): Close roots.
399
414
  // TODO(dmaretskyi): How do we handle changing to the next EPOCH?
400
- if (!this._echoHost.roots.has(handle.documentId)) {
401
- await this._echoHost.openSpaceRoot(handle.url);
415
+ const root = await this._echoHost.openSpaceRoot(handle.url);
416
+ this._databaseRoot = root;
417
+ if (root.getVersion() !== SpaceDocVersion.CURRENT) {
418
+ if (this._state !== SpaceState.REQUIRES_MIGRATION) {
419
+ this._state = SpaceState.REQUIRES_MIGRATION;
420
+ this.stateUpdate.emit();
421
+ }
402
422
  } else {
403
- log.warn('echo database root already exists', { space: this.key, rootUrl });
423
+ if (this._state !== SpaceState.READY) {
424
+ await this._enterReadyState();
425
+ }
404
426
  }
405
427
  } catch (err) {
406
428
  if (err instanceof ContextDisposedError) {
@@ -423,127 +445,46 @@ export class DataSpace {
423
445
  await this.inner.controlPipeline.writer.write({ credential: { credential } });
424
446
  }
425
447
 
426
- async createEpoch(options?: CreateEpochOptions) {
427
- let epoch: Epoch | undefined;
428
- switch (options?.migration) {
429
- case undefined:
430
- case CreateEpochRequest.Migration.NONE:
431
- {
432
- // TODO(dmaretskyi): Unify epoch construction.
433
- epoch = {
434
- previousId: this._automergeSpaceState.lastEpoch?.id,
435
- number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
436
- timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
437
- automergeRoot: this._automergeSpaceState.lastEpoch?.subject.assertion?.automergeRoot,
438
- };
439
- }
440
- break;
441
- case CreateEpochRequest.Migration.INIT_AUTOMERGE:
442
- {
443
- const document = this._echoHost.automergeRepo.create();
444
- // TODO(dmaretskyi): Unify epoch construction.
445
- epoch = {
446
- previousId: this._automergeSpaceState.lastEpoch?.id,
447
- number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
448
- timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
449
- automergeRoot: document.url,
450
- };
451
- }
452
- break;
453
- case CreateEpochRequest.Migration.PRUNE_AUTOMERGE_ROOT_HISTORY:
454
- {
455
- const currentRootUrl = this._automergeSpaceState.rootUrl;
456
- const rootHandle = this._echoHost.automergeRepo.find(currentRootUrl as any);
457
- await cancelWithContext(this._ctx, asyncTimeout(rootHandle.whenReady(), 10_000));
458
- const newRoot = this._echoHost.automergeRepo.create(rootHandle.docSync());
459
- await this._echoHost.automergeRepo.flush([newRoot.documentId]);
460
- invariant(typeof newRoot.url === 'string' && newRoot.url.length > 0);
461
- // TODO(dmaretskyi): Unify epoch construction.
462
- epoch = {
463
- previousId: this._automergeSpaceState.lastEpoch?.id,
464
- number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
465
- timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
466
- automergeRoot: newRoot.url,
467
- };
468
- }
469
- break;
470
- case CreateEpochRequest.Migration.FRAGMENT_AUTOMERGE_ROOT:
471
- {
472
- log.info('Fragmenting');
473
-
474
- const currentRootUrl = this._automergeSpaceState.rootUrl;
475
- const rootHandle = this._echoHost.automergeRepo.find<SpaceDoc>(currentRootUrl as any);
476
- await cancelWithContext(this._ctx, asyncTimeout(rootHandle.whenReady(), 10_000));
477
-
478
- // Find properties object.
479
- const objects = Object.entries((rootHandle.docSync() as SpaceDoc).objects!);
480
- const properties = findPropertiesObject(rootHandle.docSync() as SpaceDoc);
481
- const otherObjects = objects.filter(([key]) => key !== properties?.[0]);
482
- invariant(properties, 'Properties not found');
483
-
484
- // Create a new space doc with the properties object.
485
- const newSpaceDoc: SpaceDoc = { ...rootHandle.docSync(), objects: Object.fromEntries([properties]) };
486
- const newRoot = this._echoHost.automergeRepo.create(newSpaceDoc);
487
- invariant(typeof newRoot.url === 'string' && newRoot.url.length > 0);
488
-
489
- // Create new automerge documents for all objects.
490
- const docLoader = new AutomergeDocumentLoaderImpl(
491
- await createIdFromSpaceKey(this.key),
492
- this._echoHost.automergeRepo,
493
- this.key,
494
- );
495
- await docLoader.loadSpaceRootDocHandle(this._ctx, { rootUrl: newRoot.url });
496
-
497
- otherObjects.forEach(([key, value]) => {
498
- const handle = docLoader.createDocumentForObject(key);
499
- handle.change((doc: any) => {
500
- assignDeep(doc, ['objects', key], value);
501
- });
502
- });
503
-
504
- // TODO(mykola): Delete old root.
448
+ async createEpoch(options?: CreateEpochOptions): Promise<SpecificCredential<Epoch> | null> {
449
+ const ctx = this._ctx.derive();
505
450
 
506
- // TODO(dmaretskyi): Unify epoch construction.
507
- epoch = {
508
- previousId: this._automergeSpaceState.lastEpoch?.id,
509
- number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
510
- timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
511
- automergeRoot: newRoot.url,
512
- };
513
- }
514
- break;
515
- case CreateEpochRequest.Migration.REPLACE_AUTOMERGE_ROOT:
516
- {
517
- invariant(options.newAutomergeRoot);
518
- // TODO(dmaretskyi): Unify epoch construction.
519
- epoch = {
520
- previousId: this._automergeSpaceState.lastEpoch?.id,
521
- number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
522
- timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
523
- automergeRoot: options.newAutomergeRoot,
524
- };
525
- }
526
- break;
451
+ // Preserving existing behavior.
452
+ if (!options?.migration) {
453
+ return null;
527
454
  }
528
455
 
529
- if (!epoch) {
530
- return;
531
- }
456
+ const { newRoot } = await runEpochMigration(ctx, {
457
+ repo: this._echoHost.automergeRepo,
458
+ spaceId: this.id,
459
+ spaceKey: this.key,
460
+ migration: options.migration,
461
+ currentRoot: this._automergeSpaceState.rootUrl ?? null,
462
+ newAutomergeRoot: options.newAutomergeRoot,
463
+ });
532
464
 
533
- const receipt = await this.inner.controlPipeline.writer.write({
534
- credential: {
535
- credential: await this._signingContext.credentialSigner.createCredential({
536
- subject: this.key,
537
- assertion: {
538
- '@type': 'dxos.halo.credentials.Epoch',
539
- ...epoch,
540
- },
541
- }),
465
+ const epoch: Epoch = {
466
+ previousId: this._automergeSpaceState.lastEpoch?.id,
467
+ number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
468
+ timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
469
+ automergeRoot: newRoot ?? this._automergeSpaceState.rootUrl,
470
+ };
471
+
472
+ const credential = (await this._signingContext.credentialSigner.createCredential({
473
+ subject: this.key,
474
+ assertion: {
475
+ '@type': 'dxos.halo.credentials.Epoch',
476
+ ...epoch,
542
477
  },
478
+ })) as SpecificCredential<Epoch>;
479
+
480
+ const receipt = await this.inner.controlPipeline.writer.write({
481
+ credential: { credential },
543
482
  });
544
483
 
545
484
  await this.inner.controlPipeline.state.waitUntilTimeframe(new Timeframe([[receipt.feedKey, receipt.seq]]));
546
485
  await this._echoHost.updateIndexes();
486
+
487
+ return credential;
547
488
  }
548
489
 
549
490
  @synchronized
@@ -572,16 +513,3 @@ export class DataSpace {
572
513
  this.stateUpdate.emit();
573
514
  }
574
515
  }
575
-
576
- /**
577
- * Assumes properties are at root.
578
- */
579
- export const findPropertiesObject = (spaceDoc: SpaceDoc): [string, ObjectStructure] | undefined => {
580
- for (const id in spaceDoc.objects ?? {}) {
581
- const obj = spaceDoc.objects![id];
582
- if (obj.system.type?.itemId === TYPE_PROPERTIES) {
583
- return [id, obj];
584
- }
585
- }
586
- return undefined;
587
- };
@@ -0,0 +1,135 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { asyncTimeout } from '@dxos/async';
6
+ import { next as am } from '@dxos/automerge/automerge';
7
+ import type { Repo, AutomergeUrl } from '@dxos/automerge/automerge-repo';
8
+ import { cancelWithContext, type Context } from '@dxos/context';
9
+ import {
10
+ convertLegacyReferences,
11
+ convertLegacySpaceRootDoc,
12
+ findInlineObjectOfType,
13
+ migrateDocument,
14
+ } from '@dxos/echo-db';
15
+ import { AutomergeDocumentLoaderImpl } from '@dxos/echo-pipeline';
16
+ import type { SpaceDoc } from '@dxos/echo-protocol';
17
+ import { TYPE_PROPERTIES } from '@dxos/echo-schema';
18
+ import { invariant } from '@dxos/invariant';
19
+ import type { PublicKey, SpaceId } from '@dxos/keys';
20
+ import { log } from '@dxos/log';
21
+ import { CreateEpochRequest } from '@dxos/protocols/proto/dxos/client/services';
22
+ import { assignDeep } from '@dxos/util';
23
+
24
+ export type MigrationContext = {
25
+ repo: Repo;
26
+ spaceId: SpaceId;
27
+ /**
28
+ * @deprecated Remove.
29
+ */
30
+ spaceKey: PublicKey;
31
+ migration: CreateEpochRequest.Migration;
32
+ currentRoot: string | null;
33
+
34
+ /**
35
+ * For set automerge root migration type.
36
+ */
37
+ newAutomergeRoot?: string;
38
+ };
39
+
40
+ export type MigrationResult = {
41
+ newRoot?: string;
42
+ };
43
+
44
+ export const runEpochMigration = async (ctx: Context, context: MigrationContext): Promise<MigrationResult> => {
45
+ switch (context.migration) {
46
+ case CreateEpochRequest.Migration.INIT_AUTOMERGE: {
47
+ const document = context.repo.create();
48
+ await context.repo.flush();
49
+ return { newRoot: document.url };
50
+ }
51
+ case CreateEpochRequest.Migration.PRUNE_AUTOMERGE_ROOT_HISTORY: {
52
+ if (!context.currentRoot) {
53
+ throw new Error('Space does not have an automerge root');
54
+ }
55
+ const rootHandle = context.repo.find(context.currentRoot as AutomergeUrl);
56
+ await cancelWithContext(ctx, asyncTimeout(rootHandle.whenReady(), 10_000));
57
+
58
+ const newRoot = context.repo.create(rootHandle.docSync());
59
+ await context.repo.flush();
60
+ return { newRoot: newRoot.url };
61
+ }
62
+ case CreateEpochRequest.Migration.FRAGMENT_AUTOMERGE_ROOT: {
63
+ log.info('Fragmenting');
64
+
65
+ const currentRootUrl = context.currentRoot;
66
+ const rootHandle = context.repo.find<SpaceDoc>(currentRootUrl as any);
67
+ await cancelWithContext(ctx, asyncTimeout(rootHandle.whenReady(), 10_000));
68
+
69
+ // Find properties object.
70
+ const objects = Object.entries((rootHandle.docSync() as SpaceDoc).objects!);
71
+ const properties = findInlineObjectOfType(rootHandle.docSync() as SpaceDoc, TYPE_PROPERTIES);
72
+ const otherObjects = objects.filter(([key]) => key !== properties?.[0]);
73
+ invariant(properties, 'Properties not found');
74
+
75
+ // Create a new space doc with the properties object.
76
+ const newSpaceDoc: SpaceDoc = { ...rootHandle.docSync(), objects: Object.fromEntries([properties]) };
77
+ const newRoot = context.repo.create(newSpaceDoc);
78
+ invariant(typeof newRoot.url === 'string' && newRoot.url.length > 0);
79
+
80
+ // Create new automerge documents for all objects.
81
+ const docLoader = new AutomergeDocumentLoaderImpl(context.spaceId, context.repo, context.spaceKey);
82
+ await docLoader.loadSpaceRootDocHandle(ctx, { rootUrl: newRoot.url });
83
+
84
+ otherObjects.forEach(([key, value]) => {
85
+ const handle = docLoader.createDocumentForObject(key);
86
+ handle.change((doc: any) => {
87
+ assignDeep(doc, ['objects', key], value);
88
+ });
89
+ });
90
+
91
+ await context.repo.flush();
92
+ return {
93
+ newRoot: newRoot.url,
94
+ };
95
+ }
96
+ case CreateEpochRequest.Migration.MIGRATE_REFERENCES_TO_DXN: {
97
+ const currentRootUrl = context.currentRoot;
98
+ const rootHandle = context.repo.find<SpaceDoc>(currentRootUrl as any);
99
+ await cancelWithContext(ctx, asyncTimeout(rootHandle.whenReady(), 10_000));
100
+ invariant(rootHandle.docSync(), 'Root doc not found');
101
+
102
+ const newRootContent = await convertLegacySpaceRootDoc(structuredClone(rootHandle.docSync()!));
103
+
104
+ for (const [id, url] of Object.entries(newRootContent.links ?? {})) {
105
+ const handle = context.repo.find(url as any);
106
+ await cancelWithContext(ctx, asyncTimeout(handle.whenReady(), 10_000));
107
+ invariant(handle.docSync(), 'Doc not found');
108
+ const newDoc = await convertLegacyReferences(structuredClone(handle.docSync()!));
109
+ const migratedDoc = migrateDocument(handle.docSync(), newDoc);
110
+ const newHandle = context.repo.import(am.save(migratedDoc));
111
+ newRootContent.links![id] = newHandle.url;
112
+ }
113
+
114
+ const migratedRoot = migrateDocument(rootHandle.docSync(), newRootContent);
115
+ const newRoot = context.repo.import(am.save(migratedRoot));
116
+
117
+ await context.repo.flush();
118
+ return {
119
+ newRoot: newRoot.url,
120
+ };
121
+ }
122
+ // TODO(dmaretskyi): This path doesn't seem to fit here. This is not a migration.
123
+ case CreateEpochRequest.Migration.REPLACE_AUTOMERGE_ROOT: {
124
+ invariant(context.newAutomergeRoot);
125
+
126
+ // Defensive programming - it should be the responsibility of the caller to flush the new root.
127
+ await context.repo.flush();
128
+ return {
129
+ newRoot: context.newAutomergeRoot,
130
+ };
131
+ }
132
+ }
133
+
134
+ return {};
135
+ };
@@ -30,6 +30,7 @@ import {
30
30
  type UpdateSpaceRequest,
31
31
  type WriteCredentialsRequest,
32
32
  type UpdateMemberRoleRequest,
33
+ type CreateEpochResponse,
33
34
  } from '@dxos/protocols/proto/dxos/client/services';
34
35
  import { type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
35
36
  import { type GossipMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/gossip';
@@ -125,8 +126,18 @@ export class SpacesServiceImpl implements SpacesService {
125
126
  subscriptions.clear();
126
127
 
127
128
  for (const space of dataSpaceManager.spaces.values()) {
128
- // TODO(dmaretskyi): This can skip updates and not report intermediate states. Potential race condition here.
129
- subscriptions.add(space.stateUpdate.on(ctx, () => scheduler.forceTrigger()));
129
+ let lastState: SpaceState | undefined;
130
+ subscriptions.add(
131
+ space.stateUpdate.on(ctx, () => {
132
+ // Always send a separate update if the space state has changed.
133
+ if (space.state !== lastState) {
134
+ scheduler.forceTrigger();
135
+ } else {
136
+ scheduler.trigger();
137
+ }
138
+ lastState = space.state;
139
+ }),
140
+ );
130
141
 
131
142
  subscriptions.add(space.presence.updated.on(ctx, () => scheduler.trigger()));
132
143
  subscriptions.add(space.automergeSpaceState.onNewEpoch.on(ctx, () => scheduler.trigger()));
@@ -208,10 +219,11 @@ export class SpacesServiceImpl implements SpacesService {
208
219
  }
209
220
  }
210
221
 
211
- async createEpoch({ spaceKey, migration, automergeRootUrl }: CreateEpochRequest) {
222
+ async createEpoch({ spaceKey, migration, automergeRootUrl }: CreateEpochRequest): Promise<CreateEpochResponse> {
212
223
  const dataSpaceManager = await this._getDataSpaceManager();
213
224
  const space = dataSpaceManager.spaces.get(spaceKey) ?? raise(new SpaceNotFoundError(spaceKey));
214
- await space.createEpoch({ migration, newAutomergeRoot: automergeRootUrl });
225
+ const credential = await space.createEpoch({ migration, newAutomergeRoot: automergeRootUrl });
226
+ return { epochCredential: credential ?? undefined };
215
227
  }
216
228
 
217
229
  private _serializeSpace(space: DataSpace): Space {
@@ -180,10 +180,7 @@ export class TestPeer {
180
180
  }
181
181
 
182
182
  get echoHost() {
183
- return (this._props.echoHost ??= new EchoHost({
184
- kv: this.level,
185
- storage: this.storage,
186
- }));
183
+ return (this._props.echoHost ??= new EchoHost({ kv: this.level }));
187
184
  }
188
185
 
189
186
  get dataSpaceManager(): DataSpaceManager {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const DXOS_VERSION = "0.5.9-main.b8d8fee";
1
+ export const DXOS_VERSION = "0.5.9-main.bd9c8b3";