@dxos/client-services 0.5.9-main.bfee100 → 0.5.9-main.c2c4258
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-TZL7PJDX.mjs → chunk-YUU4EUWY.mjs} +1346 -821
- package/dist/lib/browser/chunk-YUU4EUWY.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-STUWVNPH.cjs → chunk-6FKYSRZO.cjs} +1480 -958
- package/dist/lib/node/chunk-6FKYSRZO.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 +33 -7
- 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 +89 -15
- package/src/packlets/spaces/data-space.ts +76 -148
- package/src/packlets/spaces/epoch-migrations.ts +135 -0
- package/src/packlets/spaces/spaces-service.ts +54 -4
- package/src/packlets/storage/index.ts +1 -0
- package/src/packlets/storage/profile-archive.ts +97 -0
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-TZL7PJDX.mjs.map +0 -7
- package/dist/lib/node/chunk-STUWVNPH.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,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
|
-
|
|
401
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
451
|
+
// Preserving existing behavior.
|
|
452
|
+
if (!options?.migration) {
|
|
453
|
+
return null;
|
|
527
454
|
}
|
|
528
455
|
|
|
529
|
-
|
|
530
|
-
|
|
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
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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,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 {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { cbor } from '@dxos/automerge/automerge-repo';
|
|
6
|
+
import { invariant } from '@dxos/invariant';
|
|
7
|
+
import type { LevelDB } from '@dxos/kv-store';
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
import { ProfileArchiveEntryType, type ProfileArchive } from '@dxos/protocols';
|
|
10
|
+
import type { Storage } from '@dxos/random-access-storage';
|
|
11
|
+
import { arrayToBuffer } from '@dxos/util';
|
|
12
|
+
|
|
13
|
+
export const encodeProfileArchive = (profile: ProfileArchive): Uint8Array => cbor.encode(profile);
|
|
14
|
+
|
|
15
|
+
export const decodeProfileArchive = (data: Uint8Array): ProfileArchive => cbor.decode(data);
|
|
16
|
+
|
|
17
|
+
export const exportProfileData = async ({
|
|
18
|
+
storage,
|
|
19
|
+
level,
|
|
20
|
+
}: {
|
|
21
|
+
storage: Storage;
|
|
22
|
+
level: LevelDB;
|
|
23
|
+
}): Promise<ProfileArchive> => {
|
|
24
|
+
const archive: ProfileArchive = { storage: [], meta: { timestamp: new Date().toISOString() } };
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
const directory = await storage.createDirectory();
|
|
28
|
+
const files = await directory.list();
|
|
29
|
+
|
|
30
|
+
log.info('begin exporting files', { count: files.length });
|
|
31
|
+
for (const filename of files) {
|
|
32
|
+
const file = await directory.getOrCreateFile(filename);
|
|
33
|
+
const { size } = await file.stat();
|
|
34
|
+
const data = await file.read(0, size);
|
|
35
|
+
archive.storage.push({
|
|
36
|
+
type: ProfileArchiveEntryType.FILE,
|
|
37
|
+
key: filename,
|
|
38
|
+
value: data,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
log.info('done exporting files', { count: files.length });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
log.info('begin exporting kv pairs');
|
|
46
|
+
const iter = await level.iterator<Uint8Array, Uint8Array>({ keyEncoding: 'binary', valueEncoding: 'binary' });
|
|
47
|
+
let count = 0;
|
|
48
|
+
for await (const [key, value] of iter) {
|
|
49
|
+
archive.storage.push({
|
|
50
|
+
type: ProfileArchiveEntryType.KEY_VALUE,
|
|
51
|
+
key,
|
|
52
|
+
value,
|
|
53
|
+
});
|
|
54
|
+
count++;
|
|
55
|
+
}
|
|
56
|
+
log.info('done exporting kv pairs', { count });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return archive;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const importProfileData = async (
|
|
63
|
+
{
|
|
64
|
+
storage,
|
|
65
|
+
level,
|
|
66
|
+
}: {
|
|
67
|
+
storage: Storage;
|
|
68
|
+
level: LevelDB;
|
|
69
|
+
},
|
|
70
|
+
archive: ProfileArchive,
|
|
71
|
+
): Promise<void> => {
|
|
72
|
+
const batch = level.batch();
|
|
73
|
+
|
|
74
|
+
for (const entry of archive.storage) {
|
|
75
|
+
switch (entry.type) {
|
|
76
|
+
case ProfileArchiveEntryType.FILE: {
|
|
77
|
+
const directory = await storage.createDirectory();
|
|
78
|
+
invariant(typeof entry.key === 'string', 'Invalid key type');
|
|
79
|
+
const file = await directory.getOrCreateFile(entry.key);
|
|
80
|
+
invariant(entry.value instanceof Uint8Array, 'Invalid value type');
|
|
81
|
+
await file.write(0, arrayToBuffer(entry.value));
|
|
82
|
+
await file.close();
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case ProfileArchiveEntryType.KEY_VALUE: {
|
|
86
|
+
invariant(entry.key instanceof Uint8Array, 'Invalid key type');
|
|
87
|
+
invariant(entry.value instanceof Uint8Array, 'Invalid value type');
|
|
88
|
+
batch.put(entry.key, entry.value, { keyEncoding: 'binary', valueEncoding: 'binary' });
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
throw new Error(`Invalid entry type: ${entry.type}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await batch.write();
|
|
97
|
+
};
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const DXOS_VERSION = "0.5.9-main.
|
|
1
|
+
export const DXOS_VERSION = "0.5.9-main.c2c4258";
|