@dxos/client-services 0.5.9-main.bf0ae3e → 0.5.9-main.bf3bb8f
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.
- package/dist/lib/browser/{chunk-4IR3JP4U.mjs → chunk-IUSAD4RP.mjs} +1405 -824
- package/dist/lib/browser/chunk-IUSAD4RP.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +13 -4
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +10 -3
- package/dist/lib/browser/packlets/testing/index.mjs.map +1 -1
- package/dist/lib/node/{chunk-ZBIDLLZ4.cjs → chunk-5PALJZPW.cjs} +1534 -956
- package/dist/lib/node/chunk-5PALJZPW.cjs.map +7 -0
- package/dist/lib/node/index.cjs +53 -44
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +17 -10
- package/dist/lib/node/packlets/testing/index.cjs.map +1 -1
- package/dist/types/src/packlets/identity/contacts-service.d.ts +14 -0
- package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -0
- package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/automerge-space-state.d.ts +4 -1
- package/dist/types/src/packlets/spaces/automerge-space-state.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +10 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +9 -9
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +23 -0
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -0
- package/dist/types/src/packlets/spaces/spaces-service.d.ts +5 -2
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/index.d.ts +1 -0
- package/dist/types/src/packlets/storage/index.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/profile-archive.d.ts +14 -0
- package/dist/types/src/packlets/storage/profile-archive.d.ts.map +1 -0
- package/dist/types/src/version.d.ts +1 -1
- package/package.json +36 -36
- package/src/packlets/identity/contacts-service.ts +85 -0
- package/src/packlets/identity/identity-service.ts +45 -13
- package/src/packlets/invitations/invitations-handler.ts +13 -5
- package/src/packlets/invitations/space-invitation-protocol.ts +11 -32
- package/src/packlets/services/service-host.ts +12 -4
- package/src/packlets/spaces/automerge-space-state.ts +11 -2
- package/src/packlets/spaces/data-space-manager.ts +90 -16
- package/src/packlets/spaces/data-space.ts +78 -148
- package/src/packlets/spaces/epoch-migrations.ts +154 -0
- package/src/packlets/spaces/spaces-service.ts +56 -4
- package/src/packlets/storage/index.ts +1 -0
- package/src/packlets/storage/profile-archive.ts +111 -0
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-4IR3JP4U.mjs.map +0 -7
- package/dist/lib/node/chunk-ZBIDLLZ4.cjs.map +0 -7
|
@@ -2,27 +2,25 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Event,
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
//
|
|
288
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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,19 @@ 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
|
-
|
|
401
|
-
|
|
415
|
+
const root = await this._echoHost.openSpaceRoot(handle.url);
|
|
416
|
+
|
|
417
|
+
// NOTE: Make sure this assignment happens synchronously together with the state change.
|
|
418
|
+
this._databaseRoot = root;
|
|
419
|
+
if (root.getVersion() !== SpaceDocVersion.CURRENT) {
|
|
420
|
+
if (this._state !== SpaceState.REQUIRES_MIGRATION) {
|
|
421
|
+
this._state = SpaceState.REQUIRES_MIGRATION;
|
|
422
|
+
this.stateUpdate.emit();
|
|
423
|
+
}
|
|
402
424
|
} else {
|
|
403
|
-
|
|
425
|
+
if (this._state !== SpaceState.READY) {
|
|
426
|
+
await this._enterReadyState();
|
|
427
|
+
}
|
|
404
428
|
}
|
|
405
429
|
} catch (err) {
|
|
406
430
|
if (err instanceof ContextDisposedError) {
|
|
@@ -423,127 +447,46 @@ export class DataSpace {
|
|
|
423
447
|
await this.inner.controlPipeline.writer.write({ credential: { credential } });
|
|
424
448
|
}
|
|
425
449
|
|
|
426
|
-
async createEpoch(options?: CreateEpochOptions) {
|
|
427
|
-
|
|
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.
|
|
450
|
+
async createEpoch(options?: CreateEpochOptions): Promise<SpecificCredential<Epoch> | null> {
|
|
451
|
+
const ctx = this._ctx.derive();
|
|
505
452
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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;
|
|
453
|
+
// Preserving existing behavior.
|
|
454
|
+
if (!options?.migration) {
|
|
455
|
+
return null;
|
|
527
456
|
}
|
|
528
457
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
458
|
+
const { newRoot } = await runEpochMigration(ctx, {
|
|
459
|
+
echoHost: this._echoHost,
|
|
460
|
+
spaceId: this.id,
|
|
461
|
+
spaceKey: this.key,
|
|
462
|
+
migration: options.migration,
|
|
463
|
+
currentRoot: this._automergeSpaceState.rootUrl ?? null,
|
|
464
|
+
newAutomergeRoot: options.newAutomergeRoot,
|
|
465
|
+
});
|
|
532
466
|
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
467
|
+
const epoch: Epoch = {
|
|
468
|
+
previousId: this._automergeSpaceState.lastEpoch?.id,
|
|
469
|
+
number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
|
|
470
|
+
timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
|
|
471
|
+
automergeRoot: newRoot ?? this._automergeSpaceState.rootUrl,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const credential = (await this._signingContext.credentialSigner.createCredential({
|
|
475
|
+
subject: this.key,
|
|
476
|
+
assertion: {
|
|
477
|
+
'@type': 'dxos.halo.credentials.Epoch',
|
|
478
|
+
...epoch,
|
|
542
479
|
},
|
|
480
|
+
})) as SpecificCredential<Epoch>;
|
|
481
|
+
|
|
482
|
+
const receipt = await this.inner.controlPipeline.writer.write({
|
|
483
|
+
credential: { credential },
|
|
543
484
|
});
|
|
544
485
|
|
|
545
486
|
await this.inner.controlPipeline.state.waitUntilTimeframe(new Timeframe([[receipt.feedKey, receipt.seq]]));
|
|
546
487
|
await this._echoHost.updateIndexes();
|
|
488
|
+
|
|
489
|
+
return credential;
|
|
547
490
|
}
|
|
548
491
|
|
|
549
492
|
@synchronized
|
|
@@ -572,16 +515,3 @@ export class DataSpace {
|
|
|
572
515
|
this.stateUpdate.emit();
|
|
573
516
|
}
|
|
574
517
|
}
|
|
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,154 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { AutomergeUrl } from '@dxos/automerge/automerge-repo';
|
|
6
|
+
import { type Context } from '@dxos/context';
|
|
7
|
+
import {
|
|
8
|
+
convertLegacyReferences,
|
|
9
|
+
convertLegacySpaceRootDoc,
|
|
10
|
+
findInlineObjectOfType,
|
|
11
|
+
migrateDocument,
|
|
12
|
+
type EchoHost,
|
|
13
|
+
} from '@dxos/echo-db';
|
|
14
|
+
import { SpaceDocVersion, type SpaceDoc } from '@dxos/echo-protocol';
|
|
15
|
+
import { TYPE_PROPERTIES } from '@dxos/echo-schema';
|
|
16
|
+
import { invariant } from '@dxos/invariant';
|
|
17
|
+
import type { PublicKey, SpaceId } from '@dxos/keys';
|
|
18
|
+
import { log } from '@dxos/log';
|
|
19
|
+
import { CreateEpochRequest } from '@dxos/protocols/proto/dxos/client/services';
|
|
20
|
+
|
|
21
|
+
export type MigrationContext = {
|
|
22
|
+
echoHost: EchoHost;
|
|
23
|
+
|
|
24
|
+
spaceId: SpaceId;
|
|
25
|
+
/**
|
|
26
|
+
* @deprecated Remove.
|
|
27
|
+
*/
|
|
28
|
+
spaceKey: PublicKey;
|
|
29
|
+
migration: CreateEpochRequest.Migration;
|
|
30
|
+
currentRoot: string | null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* For set automerge root migration type.
|
|
34
|
+
*/
|
|
35
|
+
newAutomergeRoot?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type MigrationResult = {
|
|
39
|
+
newRoot?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const LOAD_DOC_TIMEOUT = 10_000;
|
|
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.echoHost.createDoc();
|
|
48
|
+
await context.echoHost.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 = await context.echoHost.loadDoc(ctx, context.currentRoot as AutomergeUrl, {
|
|
56
|
+
timeout: LOAD_DOC_TIMEOUT,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const newRoot = context.echoHost.createDoc(rootHandle.docSync());
|
|
60
|
+
await context.echoHost.flush();
|
|
61
|
+
return { newRoot: newRoot.url };
|
|
62
|
+
}
|
|
63
|
+
case CreateEpochRequest.Migration.FRAGMENT_AUTOMERGE_ROOT: {
|
|
64
|
+
log.info('Fragmenting');
|
|
65
|
+
|
|
66
|
+
const currentRootUrl = context.currentRoot;
|
|
67
|
+
const rootHandle = await context.echoHost.loadDoc<SpaceDoc>(ctx, currentRootUrl as any, {
|
|
68
|
+
timeout: LOAD_DOC_TIMEOUT,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Find properties object.
|
|
72
|
+
const objects = Object.entries((rootHandle.docSync() as SpaceDoc).objects!);
|
|
73
|
+
const properties = findInlineObjectOfType(rootHandle.docSync() as SpaceDoc, TYPE_PROPERTIES);
|
|
74
|
+
const otherObjects = objects.filter(([key]) => key !== properties?.[0]);
|
|
75
|
+
invariant(properties, 'Properties not found');
|
|
76
|
+
|
|
77
|
+
// Create a new space doc with the properties object.
|
|
78
|
+
const newRoot = context.echoHost.createDoc({
|
|
79
|
+
...rootHandle.docSync(),
|
|
80
|
+
objects: Object.fromEntries([properties]),
|
|
81
|
+
});
|
|
82
|
+
invariant(typeof newRoot.url === 'string' && newRoot.url.length > 0);
|
|
83
|
+
|
|
84
|
+
// Create new automerge documents for all objects.
|
|
85
|
+
const newLinks: [string, AutomergeUrl][] = [];
|
|
86
|
+
for (const [id, objData] of otherObjects) {
|
|
87
|
+
const handle = context.echoHost.createDoc<SpaceDoc>({
|
|
88
|
+
version: SpaceDocVersion.CURRENT,
|
|
89
|
+
access: {
|
|
90
|
+
spaceKey: context.spaceKey.toHex(),
|
|
91
|
+
},
|
|
92
|
+
objects: {
|
|
93
|
+
[id]: objData,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
newLinks.push([id, handle.url]);
|
|
97
|
+
}
|
|
98
|
+
newRoot.change((doc: SpaceDoc) => {
|
|
99
|
+
doc.links ??= {};
|
|
100
|
+
for (const [id, url] of newLinks) {
|
|
101
|
+
doc.links[id] = url;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await context.echoHost.flush();
|
|
106
|
+
return {
|
|
107
|
+
newRoot: newRoot.url,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
case CreateEpochRequest.Migration.MIGRATE_REFERENCES_TO_DXN: {
|
|
111
|
+
const currentRootUrl = context.currentRoot;
|
|
112
|
+
const rootHandle = await context.echoHost.loadDoc<SpaceDoc>(ctx, currentRootUrl as any, {
|
|
113
|
+
timeout: LOAD_DOC_TIMEOUT,
|
|
114
|
+
});
|
|
115
|
+
invariant(rootHandle.docSync(), 'Root doc not found');
|
|
116
|
+
|
|
117
|
+
const newRootContent = await convertLegacySpaceRootDoc(structuredClone(rootHandle.docSync()!));
|
|
118
|
+
|
|
119
|
+
for (const [id, url] of Object.entries(newRootContent.links ?? {})) {
|
|
120
|
+
try {
|
|
121
|
+
const handle = await context.echoHost.loadDoc(ctx, url as any, { timeout: LOAD_DOC_TIMEOUT });
|
|
122
|
+
invariant(handle.docSync());
|
|
123
|
+
const newDoc = await convertLegacyReferences(structuredClone(handle.docSync()!));
|
|
124
|
+
const migratedDoc = migrateDocument(handle.docSync(), newDoc);
|
|
125
|
+
const newHandle = context.echoHost.createDoc(migratedDoc, { preserveHistory: true });
|
|
126
|
+
newRootContent.links![id] = newHandle.url;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
log.warn('Failed to migrate reference', { id, url, error: err });
|
|
129
|
+
delete newRootContent.links![id];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const migratedRoot = migrateDocument(rootHandle.docSync(), newRootContent);
|
|
134
|
+
const newRoot = context.echoHost.createDoc(migratedRoot, { preserveHistory: true });
|
|
135
|
+
|
|
136
|
+
await context.echoHost.flush();
|
|
137
|
+
return {
|
|
138
|
+
newRoot: newRoot.url,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// TODO(dmaretskyi): This path doesn't seem to fit here. This is not a migration.
|
|
142
|
+
case CreateEpochRequest.Migration.REPLACE_AUTOMERGE_ROOT: {
|
|
143
|
+
invariant(context.newAutomergeRoot);
|
|
144
|
+
|
|
145
|
+
// Defensive programming - it should be the responsibility of the caller to flush the new root.
|
|
146
|
+
await context.echoHost.flush();
|
|
147
|
+
return {
|
|
148
|
+
newRoot: context.newAutomergeRoot,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {};
|
|
154
|
+
};
|
|
@@ -30,6 +30,11 @@ import {
|
|
|
30
30
|
type UpdateSpaceRequest,
|
|
31
31
|
type WriteCredentialsRequest,
|
|
32
32
|
type UpdateMemberRoleRequest,
|
|
33
|
+
type AdmitContactRequest,
|
|
34
|
+
type ContactAdmission,
|
|
35
|
+
type JoinSpaceResponse,
|
|
36
|
+
type JoinBySpaceKeyRequest,
|
|
37
|
+
type CreateEpochResponse,
|
|
33
38
|
} from '@dxos/protocols/proto/dxos/client/services';
|
|
34
39
|
import { type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
35
40
|
import { type GossipMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/gossip';
|
|
@@ -125,8 +130,18 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
125
130
|
subscriptions.clear();
|
|
126
131
|
|
|
127
132
|
for (const space of dataSpaceManager.spaces.values()) {
|
|
128
|
-
|
|
129
|
-
subscriptions.add(
|
|
133
|
+
let lastState: SpaceState | undefined;
|
|
134
|
+
subscriptions.add(
|
|
135
|
+
space.stateUpdate.on(ctx, () => {
|
|
136
|
+
// Always send a separate update if the space state has changed.
|
|
137
|
+
if (space.state !== lastState) {
|
|
138
|
+
scheduler.forceTrigger();
|
|
139
|
+
} else {
|
|
140
|
+
scheduler.trigger();
|
|
141
|
+
}
|
|
142
|
+
lastState = space.state;
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
130
145
|
|
|
131
146
|
subscriptions.add(space.presence.updated.on(ctx, () => scheduler.trigger()));
|
|
132
147
|
subscriptions.add(space.automergeSpaceState.onNewEpoch.on(ctx, () => scheduler.trigger()));
|
|
@@ -208,10 +223,45 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
208
223
|
}
|
|
209
224
|
}
|
|
210
225
|
|
|
211
|
-
async createEpoch({ spaceKey, migration, automergeRootUrl }: CreateEpochRequest) {
|
|
226
|
+
async createEpoch({ spaceKey, migration, automergeRootUrl }: CreateEpochRequest): Promise<CreateEpochResponse> {
|
|
212
227
|
const dataSpaceManager = await this._getDataSpaceManager();
|
|
213
228
|
const space = dataSpaceManager.spaces.get(spaceKey) ?? raise(new SpaceNotFoundError(spaceKey));
|
|
214
|
-
await space.createEpoch({ migration, newAutomergeRoot: automergeRootUrl });
|
|
229
|
+
const credential = await space.createEpoch({ migration, newAutomergeRoot: automergeRootUrl });
|
|
230
|
+
return { epochCredential: credential ?? undefined };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async admitContact(request: AdmitContactRequest): Promise<void> {
|
|
234
|
+
const dataSpaceManager = await this._getDataSpaceManager();
|
|
235
|
+
await dataSpaceManager.admitMember({
|
|
236
|
+
spaceKey: request.spaceKey,
|
|
237
|
+
identityKey: request.contact.identityKey,
|
|
238
|
+
role: request.role,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async joinBySpaceKey({ spaceKey }: JoinBySpaceKeyRequest): Promise<JoinSpaceResponse> {
|
|
243
|
+
const dataSpaceManager = await this._getDataSpaceManager();
|
|
244
|
+
const credential = await dataSpaceManager.requestSpaceAdmissionCredential(spaceKey);
|
|
245
|
+
return this._joinByAdmission({ credential });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async _joinByAdmission({ credential }: ContactAdmission): Promise<JoinSpaceResponse> {
|
|
249
|
+
const assertion = getCredentialAssertion(credential);
|
|
250
|
+
invariant(assertion['@type'] === 'dxos.halo.credentials.SpaceMember', 'Invalid credential');
|
|
251
|
+
const myIdentity = this._identityManager.identity;
|
|
252
|
+
invariant(myIdentity && credential.subject.id.equals(myIdentity.identityKey));
|
|
253
|
+
|
|
254
|
+
const dataSpaceManager = await this._getDataSpaceManager();
|
|
255
|
+
let dataSpace = dataSpaceManager.spaces.get(assertion.spaceKey);
|
|
256
|
+
if (!dataSpace) {
|
|
257
|
+
dataSpace = await dataSpaceManager.acceptSpace({
|
|
258
|
+
spaceKey: assertion.spaceKey,
|
|
259
|
+
genesisFeedKey: assertion.genesisFeedKey,
|
|
260
|
+
});
|
|
261
|
+
await myIdentity.controlPipeline.writer.write({ credential: { credential } });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { space: this._serializeSpace(dataSpace) };
|
|
215
265
|
}
|
|
216
266
|
|
|
217
267
|
private _serializeSpace(space: DataSpace): Space {
|
|
@@ -234,6 +284,8 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
234
284
|
currentDataTimeframe: undefined,
|
|
235
285
|
targetDataTimeframe: undefined,
|
|
236
286
|
totalDataTimeframe: undefined,
|
|
287
|
+
|
|
288
|
+
spaceRootUrl: space.databaseRoot?.url,
|
|
237
289
|
},
|
|
238
290
|
members: Array.from(space.inner.spaceState.members.values()).map((member) => {
|
|
239
291
|
const peers = space.presence.getPeersOnline().filter(({ identityKey }) => identityKey.equals(member.key));
|