@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.
- package/dist/lib/browser/{chunk-2EFXBSRZ.mjs → chunk-KI7FY3ZO.mjs} +718 -555
- package/dist/lib/browser/chunk-KI7FY3ZO.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -3
- 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 +2 -3
- package/dist/lib/browser/packlets/testing/index.mjs.map +2 -2
- package/dist/lib/node/{chunk-2SS7JAIR.cjs → chunk-XWMOEZYI.cjs} +869 -709
- package/dist/lib/node/chunk-XWMOEZYI.cjs.map +7 -0
- package/dist/lib/node/index.cjs +41 -43
- 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 +9 -10
- package/dist/lib/node/packlets/testing/index.cjs.map +2 -2
- package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.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.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 +2 -2
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/package.json +36 -36
- package/src/packlets/identity/identity-service.ts +33 -7
- package/src/packlets/services/service-context.ts +1 -4
- package/src/packlets/spaces/automerge-space-state.ts +11 -2
- package/src/packlets/spaces/data-space-manager.ts +35 -14
- 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 +16 -4
- package/src/packlets/testing/test-builder.ts +1 -4
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-2EFXBSRZ.mjs.map +0 -7
- 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
|
|
7
|
+
import { type AutomergeUrl, type DocHandle } from '@dxos/automerge/automerge-repo';
|
|
8
8
|
import { PropertiesType } from '@dxos/client-protocol';
|
|
9
|
-
import {
|
|
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 {
|
|
26
|
-
|
|
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
|
|
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 {
|
|
48
|
+
import { ComplexMap, assignDeep, deferFunction, forEachAsync } from '@dxos/util';
|
|
43
49
|
|
|
44
|
-
import { DataSpace
|
|
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 &&
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 =
|
|
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,
|
|
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,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
|
-
|
|
129
|
-
subscriptions.add(
|
|
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.
|
|
1
|
+
export const DXOS_VERSION = "0.5.9-main.bd9c8b3";
|