@dxos/client-services 0.8.4-main.f5c0578 → 0.8.4-main.fcfe5033a5
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-WBMHLEAB.mjs → chunk-HYGNOM23.mjs} +2843 -2114
- package/dist/lib/browser/chunk-HYGNOM23.mjs.map +7 -0
- package/dist/lib/browser/chunk-NQSC7HOE.mjs +22 -0
- package/dist/lib/browser/chunk-NQSC7HOE.mjs.map +7 -0
- package/dist/lib/browser/chunk-QCWEHHJW.mjs +24 -0
- package/dist/lib/browser/chunk-QCWEHHJW.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +512 -90
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/diagnostics/browser-diagnostics-broadcast.mjs +93 -0
- package/dist/lib/browser/packlets/diagnostics/browser-diagnostics-broadcast.mjs.map +7 -0
- package/dist/lib/browser/packlets/diagnostics/diagnostics-broadcast.mjs +11 -0
- package/dist/lib/browser/packlets/diagnostics/diagnostics-broadcast.mjs.map +7 -0
- package/dist/lib/browser/packlets/locks/browser.mjs +126 -0
- package/dist/lib/browser/packlets/locks/browser.mjs.map +7 -0
- package/dist/lib/browser/packlets/locks/node.mjs +66 -0
- package/dist/lib/browser/packlets/locks/node.mjs.map +7 -0
- package/dist/lib/browser/testing/index.mjs +37 -19
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/chunk-2SZHAWBN.mjs +24 -0
- package/dist/lib/node-esm/chunk-2SZHAWBN.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-IPVFX6KI.mjs → chunk-GFT7MAQE.mjs} +2571 -1711
- package/dist/lib/node-esm/chunk-GFT7MAQE.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-PKEGMOQ4.mjs +22 -0
- package/dist/lib/node-esm/chunk-PKEGMOQ4.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +512 -90
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/packlets/diagnostics/browser-diagnostics-broadcast.mjs +93 -0
- package/dist/lib/node-esm/packlets/diagnostics/browser-diagnostics-broadcast.mjs.map +7 -0
- package/dist/lib/node-esm/packlets/diagnostics/diagnostics-broadcast.mjs +11 -0
- package/dist/lib/node-esm/packlets/diagnostics/diagnostics-broadcast.mjs.map +7 -0
- package/dist/lib/node-esm/packlets/locks/browser.mjs +126 -0
- package/dist/lib/node-esm/packlets/locks/browser.mjs.map +7 -0
- package/dist/lib/node-esm/packlets/locks/node.mjs +66 -0
- package/dist/lib/node-esm/packlets/locks/node.mjs.map +7 -0
- package/dist/lib/node-esm/testing/index.mjs +37 -19
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts +3 -2
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts +2 -1
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts.map +1 -1
- package/dist/types/src/packlets/devtools/devtools.d.ts +2 -2
- package/dist/types/src/packlets/devtools/devtools.d.ts.map +1 -1
- package/dist/types/src/packlets/diagnostics/diagnostics.d.ts +1 -2
- package/dist/types/src/packlets/diagnostics/diagnostics.d.ts.map +1 -1
- package/dist/types/src/packlets/diagnostics/index.d.ts +1 -1
- package/dist/types/src/packlets/diagnostics/index.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/authenticator.d.ts +2 -2
- package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-manager.d.ts +6 -6
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts +7 -6
- package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-service.d.ts +6 -10
- package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +8 -11
- package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +6 -5
- package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +1 -1
- package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +7 -4
- package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts +4 -4
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-manager.d.ts +3 -3
- package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-service.d.ts +3 -3
- package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +4 -3
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/locks/index.d.ts +1 -1
- package/dist/types/src/packlets/locks/index.d.ts.map +1 -1
- package/dist/types/src/packlets/logging/logging-service.d.ts +4 -0
- package/dist/types/src/packlets/logging/logging-service.d.ts.map +1 -1
- package/dist/types/src/packlets/network/network-service.d.ts +5 -4
- package/dist/types/src/packlets/network/network-service.d.ts.map +1 -1
- package/dist/types/src/packlets/services/client-rpc-server.d.ts +5 -5
- package/dist/types/src/packlets/services/client-rpc-server.d.ts.map +1 -1
- package/dist/types/src/packlets/services/feed-syncer.d.ts +59 -0
- package/dist/types/src/packlets/services/feed-syncer.d.ts.map +1 -0
- package/dist/types/src/packlets/services/feed-syncer.test.d.ts +2 -0
- package/dist/types/src/packlets/services/feed-syncer.test.d.ts.map +1 -0
- package/dist/types/src/packlets/services/platform.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +13 -8
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +20 -7
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/space-export/archive-format.d.ts +9 -0
- package/dist/types/src/packlets/space-export/archive-format.d.ts.map +1 -0
- package/dist/types/src/packlets/space-export/index.d.ts +4 -1
- package/dist/types/src/packlets/space-export/index.d.ts.map +1 -1
- package/dist/types/src/packlets/space-export/serialized-space-reader.d.ts +23 -0
- package/dist/types/src/packlets/space-export/serialized-space-reader.d.ts.map +1 -0
- package/dist/types/src/packlets/space-export/serialized-space-writer.d.ts +36 -0
- package/dist/types/src/packlets/space-export/serialized-space-writer.d.ts.map +1 -0
- package/dist/types/src/packlets/space-export/space-archive-reader.d.ts +9 -1
- package/dist/types/src/packlets/space-export/space-archive-reader.d.ts.map +1 -1
- package/dist/types/src/packlets/space-export/space-archive-writer.d.ts +7 -1
- package/dist/types/src/packlets/space-export/space-archive-writer.d.ts.map +1 -1
- package/dist/types/src/packlets/space-export/space-archive.test.d.ts +2 -0
- package/dist/types/src/packlets/space-export/space-archive.test.d.ts.map +1 -0
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +28 -16
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +26 -9
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +2 -2
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/genesis.d.ts +2 -1
- package/dist/types/src/packlets/spaces/genesis.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +6 -6
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts +17 -7
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/invitation-utils.d.ts +6 -3
- package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +6 -5
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/worker/worker-runtime.d.ts +41 -4
- package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
- package/dist/types/src/packlets/worker/worker-session.d.ts +2 -4
- package/dist/types/src/packlets/worker/worker-session.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/dist/types/src/version.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +70 -48
- package/src/index.ts +1 -0
- package/src/packlets/agents/edge-agent-manager.ts +10 -7
- package/src/packlets/agents/edge-agent-service.ts +15 -3
- package/src/packlets/devices/devices-service.test.ts +0 -1
- package/src/packlets/devices/devices-service.ts +1 -1
- package/src/packlets/devtools/devtools.ts +2 -3
- package/src/packlets/diagnostics/diagnostics.ts +1 -2
- package/src/packlets/diagnostics/index.ts +1 -1
- package/src/packlets/identity/authenticator.ts +2 -2
- package/src/packlets/identity/contacts-service.ts +0 -1
- package/src/packlets/identity/identity-manager.test.ts +5 -5
- package/src/packlets/identity/identity-manager.ts +21 -18
- package/src/packlets/identity/identity-recovery-manager.ts +22 -18
- package/src/packlets/identity/identity-service.test.ts +6 -27
- package/src/packlets/identity/identity-service.ts +13 -81
- package/src/packlets/identity/identity.test.ts +6 -6
- package/src/packlets/identity/identity.ts +11 -34
- package/src/packlets/invitations/device-invitation-protocol.ts +8 -7
- package/src/packlets/invitations/edge-invitation-handler.ts +9 -5
- package/src/packlets/invitations/invitation-guest-extenstion.ts +6 -4
- package/src/packlets/invitations/invitation-host-extension.ts +6 -4
- package/src/packlets/invitations/invitation-protocol.ts +7 -4
- package/src/packlets/invitations/invitation-state.ts +0 -10
- package/src/packlets/invitations/invitations-handler.test.ts +4 -5
- package/src/packlets/invitations/invitations-handler.ts +69 -13
- package/src/packlets/invitations/invitations-manager.ts +40 -15
- package/src/packlets/invitations/invitations-service.ts +9 -9
- package/src/packlets/invitations/space-invitation-protocol.test.ts +17 -16
- package/src/packlets/invitations/space-invitation-protocol.ts +11 -16
- package/src/packlets/locks/index.ts +1 -1
- package/src/packlets/logging/logging-service.ts +5 -1
- package/src/packlets/network/network-service.test.ts +0 -1
- package/src/packlets/network/network-service.ts +10 -8
- package/src/packlets/services/client-rpc-server.ts +19 -16
- package/src/packlets/services/feed-syncer.test.ts +340 -0
- package/src/packlets/services/feed-syncer.ts +337 -0
- package/src/packlets/services/platform.ts +7 -1
- package/src/packlets/services/service-context.test.ts +3 -2
- package/src/packlets/services/service-context.ts +141 -55
- package/src/packlets/services/service-host.test.ts +8 -8
- package/src/packlets/services/service-host.ts +77 -32
- package/src/packlets/services/service-registry.test.ts +0 -1
- package/src/packlets/space-export/archive-format.ts +42 -0
- package/src/packlets/space-export/index.ts +4 -1
- package/src/packlets/space-export/serialized-space-reader.ts +111 -0
- package/src/packlets/space-export/serialized-space-writer.ts +246 -0
- package/src/packlets/space-export/space-archive-reader.ts +65 -4
- package/src/packlets/space-export/space-archive-writer.ts +43 -5
- package/src/packlets/space-export/space-archive.test.ts +461 -0
- package/src/packlets/spaces/data-space-manager.test.ts +79 -13
- package/src/packlets/spaces/data-space-manager.ts +122 -110
- package/src/packlets/spaces/data-space.ts +58 -33
- package/src/packlets/spaces/edge-feed-replicator.test.ts +2 -2
- package/src/packlets/spaces/edge-feed-replicator.ts +12 -10
- package/src/packlets/spaces/epoch-migrations.ts +5 -5
- package/src/packlets/spaces/genesis.ts +6 -1
- package/src/packlets/spaces/notarization-plugin.test.ts +2 -2
- package/src/packlets/spaces/notarization-plugin.ts +10 -9
- package/src/packlets/spaces/spaces-service.test.ts +18 -11
- package/src/packlets/spaces/spaces-service.ts +124 -25
- package/src/packlets/storage/storage.ts +4 -4
- package/src/packlets/testing/invitation-utils.ts +10 -6
- package/src/packlets/testing/test-builder.ts +36 -10
- package/src/packlets/worker/worker-runtime.ts +179 -17
- package/src/packlets/worker/worker-session.ts +12 -18
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-WBMHLEAB.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-IPVFX6KI.mjs.map +0 -7
- package/dist/types/src/packlets/identity/default-space-state-machine.d.ts +0 -19
- package/dist/types/src/packlets/identity/default-space-state-machine.d.ts.map +0 -1
- package/src/packlets/identity/default-space-state-machine.ts +0 -44
|
@@ -6,19 +6,33 @@ import type { DocumentId } from '@automerge/automerge-repo';
|
|
|
6
6
|
|
|
7
7
|
import { assertArgument, failedInvariant, invariant } from '@dxos/invariant';
|
|
8
8
|
import { log } from '@dxos/log';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type FeedArchiveBlock,
|
|
11
|
+
type FeedArchiveMetadata,
|
|
12
|
+
SpaceArchiveFileStructure,
|
|
13
|
+
type SpaceArchiveMetadata,
|
|
14
|
+
} from '@dxos/protocols';
|
|
10
15
|
import type { SpaceArchive } from '@dxos/protocols/proto/dxos/client/services';
|
|
11
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Extracted feed data from the archive.
|
|
19
|
+
*/
|
|
20
|
+
export type ExtractedFeed = {
|
|
21
|
+
metadata: FeedArchiveMetadata;
|
|
22
|
+
blocks: FeedArchiveBlock[];
|
|
23
|
+
};
|
|
24
|
+
|
|
12
25
|
export type ExtractedSpaceArchive = {
|
|
13
26
|
metadata: SpaceArchiveMetadata;
|
|
14
27
|
documents: Record<DocumentId, Uint8Array>;
|
|
28
|
+
feeds: Record<string, ExtractedFeed>;
|
|
15
29
|
};
|
|
16
30
|
|
|
17
31
|
export const extractSpaceArchive = async (archive: SpaceArchive): Promise<ExtractedSpaceArchive> => {
|
|
18
32
|
const { Archive } = await import('@obsidize/tar-browserify');
|
|
19
33
|
const { entries } = await Archive.extract(archive.contents);
|
|
20
34
|
const metadataEntry = entries.find((entry) => entry.fileName === SpaceArchiveFileStructure.metadata);
|
|
21
|
-
assertArgument(metadataEntry, 'Metadata entry not found');
|
|
35
|
+
assertArgument(metadataEntry, 'metadataEntry', 'Metadata entry not found');
|
|
22
36
|
const metadata = JSON.parse(metadataEntry.getContentAsText());
|
|
23
37
|
const documents: Record<DocumentId, Uint8Array> = {};
|
|
24
38
|
for (const entry of entries.filter((entry) => entry.fileName.startsWith(`${SpaceArchiveFileStructure.documents}/`))) {
|
|
@@ -29,6 +43,53 @@ export const extractSpaceArchive = async (archive: SpaceArchive): Promise<Extrac
|
|
|
29
43
|
documents[documentId] = entry.content ?? failedInvariant();
|
|
30
44
|
}
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
const feeds: Record<string, ExtractedFeed> = {};
|
|
47
|
+
const feedsPrefix = `${SpaceArchiveFileStructure.feeds}/`;
|
|
48
|
+
const feedEntries = entries.filter((entry) => entry.fileName.startsWith(feedsPrefix));
|
|
49
|
+
|
|
50
|
+
const feedMetadataByFeedId = new Map<string, FeedArchiveMetadata>();
|
|
51
|
+
const feedBlocksByFeedId = new Map<string, Map<number, FeedArchiveBlock[]>>();
|
|
52
|
+
|
|
53
|
+
for (const entry of feedEntries) {
|
|
54
|
+
const relativePath = entry.fileName.slice(feedsPrefix.length);
|
|
55
|
+
const pathParts = relativePath.split('/');
|
|
56
|
+
if (pathParts.length !== 2) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const [feedId, fileName] = pathParts;
|
|
61
|
+
invariant(feedId, 'Feed ID is required');
|
|
62
|
+
invariant(fileName, 'File name is required');
|
|
63
|
+
|
|
64
|
+
if (fileName === SpaceArchiveFileStructure.feedMetadata) {
|
|
65
|
+
const feedMetadata = JSON.parse(entry.getContentAsText()) as FeedArchiveMetadata;
|
|
66
|
+
feedMetadataByFeedId.set(feedId, feedMetadata);
|
|
67
|
+
} else if (fileName.startsWith(SpaceArchiveFileStructure.feedBlocksPrefix) && fileName.endsWith('.json')) {
|
|
68
|
+
const chunkIndexStr = fileName.slice(SpaceArchiveFileStructure.feedBlocksPrefix.length).replace(/\.json$/, '');
|
|
69
|
+
const chunkIndex = parseInt(chunkIndexStr, 10);
|
|
70
|
+
invariant(!isNaN(chunkIndex), `Invalid chunk index: ${chunkIndexStr}`);
|
|
71
|
+
|
|
72
|
+
const blocks = JSON.parse(entry.getContentAsText()) as FeedArchiveBlock[];
|
|
73
|
+
if (!feedBlocksByFeedId.has(feedId)) {
|
|
74
|
+
feedBlocksByFeedId.set(feedId, new Map());
|
|
75
|
+
}
|
|
76
|
+
feedBlocksByFeedId.get(feedId)!.set(chunkIndex, blocks);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const [feedId, feedMetadata] of feedMetadataByFeedId) {
|
|
81
|
+
const blockChunks = feedBlocksByFeedId.get(feedId) ?? new Map<number, FeedArchiveBlock[]>();
|
|
82
|
+
const sortedChunkIndices = Array.from(blockChunks.keys()).sort((a, b) => a - b);
|
|
83
|
+
const allBlocks: FeedArchiveBlock[] = [];
|
|
84
|
+
for (const chunkIndex of sortedChunkIndices) {
|
|
85
|
+
allBlocks.push(...blockChunks.get(chunkIndex)!);
|
|
86
|
+
}
|
|
87
|
+
feeds[feedId] = {
|
|
88
|
+
metadata: feedMetadata,
|
|
89
|
+
blocks: allBlocks,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
log('extracted space archive', { metadata, documents, feedCount: Object.keys(feeds).length });
|
|
94
|
+
return { metadata, documents, feeds };
|
|
34
95
|
};
|
|
@@ -7,8 +7,16 @@ import type * as tar from '@obsidize/tar-browserify';
|
|
|
7
7
|
import { type Context, Resource } from '@dxos/context';
|
|
8
8
|
import { assertArgument, assertState } from '@dxos/invariant';
|
|
9
9
|
import type { IdentityDid, SpaceId } from '@dxos/keys';
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
FEED_ARCHIVE_BLOCKS_PER_CHUNK,
|
|
12
|
+
type FeedArchiveBlock,
|
|
13
|
+
type FeedArchiveMetadata,
|
|
14
|
+
SpaceArchiveFileStructure,
|
|
15
|
+
type SpaceArchiveMetadata,
|
|
16
|
+
SpaceArchiveVersion,
|
|
17
|
+
} from '@dxos/protocols';
|
|
18
|
+
import { SpaceArchive } from '@dxos/protocols/proto/dxos/client/services';
|
|
19
|
+
import { createFilename } from '@dxos/util';
|
|
12
20
|
|
|
13
21
|
export type SpaceArchiveBeginProps = {
|
|
14
22
|
spaceId?: SpaceId;
|
|
@@ -44,21 +52,50 @@ export class SpaceArchiveWriter extends Resource {
|
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
async setCurrentRootUrl(url: string): Promise<void> {
|
|
47
|
-
assertArgument(url.startsWith('automerge:'), 'Invalid root URL');
|
|
55
|
+
assertArgument(url.startsWith('automerge:'), 'url', 'Invalid root URL');
|
|
48
56
|
assertState(this._tar, 'Not open');
|
|
49
57
|
assertState(this._meta, 'Not started');
|
|
50
58
|
this._currentRootUrl = url;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
async writeDocument(documentId: string, data: Uint8Array): Promise<void> {
|
|
54
|
-
assertArgument(!documentId.startsWith('automerge:'), 'Invalid document ID');
|
|
62
|
+
assertArgument(!documentId.startsWith('automerge:'), 'documentId', 'Invalid document ID');
|
|
55
63
|
assertState(this._archive, 'Not open');
|
|
56
64
|
this._archive.addBinaryFile(`${SpaceArchiveFileStructure.documents}/${documentId}.bin`, data);
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Writes a feed with its metadata and blocks to the archive.
|
|
69
|
+
* Blocks are written in chunks of {@link FEED_ARCHIVE_BLOCKS_PER_CHUNK}.
|
|
70
|
+
*/
|
|
71
|
+
async writeFeed(feedId: string, namespace: string, blocks: FeedArchiveBlock[]): Promise<void> {
|
|
72
|
+
assertArgument(feedId, 'feedId', 'Feed ID is required');
|
|
73
|
+
assertArgument(namespace, 'namespace', 'Namespace is required');
|
|
74
|
+
assertState(this._archive, 'Not open');
|
|
75
|
+
|
|
76
|
+
const feedPath = `${SpaceArchiveFileStructure.feeds}/${feedId}`;
|
|
77
|
+
|
|
78
|
+
const metadata: FeedArchiveMetadata = {
|
|
79
|
+
id: feedId,
|
|
80
|
+
namespace,
|
|
81
|
+
};
|
|
82
|
+
this._archive.addTextFile(`${feedPath}/${SpaceArchiveFileStructure.feedMetadata}`, JSON.stringify(metadata));
|
|
83
|
+
|
|
84
|
+
for (let chunkIndex = 0; chunkIndex * FEED_ARCHIVE_BLOCKS_PER_CHUNK < blocks.length; chunkIndex++) {
|
|
85
|
+
const start = chunkIndex * FEED_ARCHIVE_BLOCKS_PER_CHUNK;
|
|
86
|
+
const end = Math.min(start + FEED_ARCHIVE_BLOCKS_PER_CHUNK, blocks.length);
|
|
87
|
+
const chunk = blocks.slice(start, end);
|
|
88
|
+
this._archive.addTextFile(
|
|
89
|
+
`${feedPath}/${SpaceArchiveFileStructure.feedBlocksPrefix}${chunkIndex}.json`,
|
|
90
|
+
JSON.stringify(chunk),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
59
95
|
async finish(): Promise<SpaceArchive> {
|
|
60
96
|
assertState(this._archive, 'Not open');
|
|
61
97
|
assertState(this._meta, 'Not started');
|
|
98
|
+
assertState(this._meta.spaceId, 'No space ID set');
|
|
62
99
|
assertState(this._currentRootUrl, 'No root URL set');
|
|
63
100
|
|
|
64
101
|
const metadata: SpaceArchiveMetadata = {
|
|
@@ -76,8 +113,9 @@ export class SpaceArchiveWriter extends Resource {
|
|
|
76
113
|
const binary = this._archive.toUint8Array();
|
|
77
114
|
|
|
78
115
|
return {
|
|
79
|
-
filename:
|
|
116
|
+
filename: createFilename({ parts: [this._meta.spaceId], ext: 'tar' }),
|
|
80
117
|
contents: binary,
|
|
118
|
+
format: SpaceArchive.Format.BINARY,
|
|
81
119
|
};
|
|
82
120
|
}
|
|
83
121
|
}
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { DocumentId } from '@automerge/automerge-repo';
|
|
6
|
+
import { describe, expect, test } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { type SerializedSpace } from '@dxos/echo-db';
|
|
9
|
+
import { ObjectId, SpaceId } from '@dxos/keys';
|
|
10
|
+
import {
|
|
11
|
+
FEED_ARCHIVE_BLOCKS_PER_CHUNK,
|
|
12
|
+
type FeedArchiveBlock,
|
|
13
|
+
SpaceArchiveFileStructure,
|
|
14
|
+
SpaceArchiveVersion,
|
|
15
|
+
} from '@dxos/protocols';
|
|
16
|
+
import { SpaceArchive } from '@dxos/protocols/proto/dxos/client/services';
|
|
17
|
+
|
|
18
|
+
import { detectSpaceArchiveFormat } from './archive-format';
|
|
19
|
+
import { buildDatabaseDirectoryFromObjects, readSerializedSpaceArchive } from './serialized-space-reader';
|
|
20
|
+
import { objectStructureToObjJson, orderObjJsonFields } from './serialized-space-writer';
|
|
21
|
+
import { extractSpaceArchive } from './space-archive-reader';
|
|
22
|
+
import { SpaceArchiveWriter } from './space-archive-writer';
|
|
23
|
+
|
|
24
|
+
describe('SpaceArchive', () => {
|
|
25
|
+
describe('SpaceArchiveWriter', () => {
|
|
26
|
+
test('writes and reads documents', async () => {
|
|
27
|
+
const writer = new SpaceArchiveWriter();
|
|
28
|
+
await writer.open();
|
|
29
|
+
try {
|
|
30
|
+
const spaceId = SpaceId.random();
|
|
31
|
+
await writer.begin({ spaceId });
|
|
32
|
+
await writer.setCurrentRootUrl('automerge:test123');
|
|
33
|
+
await writer.writeDocument('doc1', new Uint8Array([1, 2, 3]));
|
|
34
|
+
await writer.writeDocument('doc2', new Uint8Array([4, 5, 6]));
|
|
35
|
+
|
|
36
|
+
const archive = await writer.finish();
|
|
37
|
+
|
|
38
|
+
expect(archive.filename).toContain(spaceId);
|
|
39
|
+
expect(archive.contents).toBeInstanceOf(Uint8Array);
|
|
40
|
+
|
|
41
|
+
const extracted = await extractSpaceArchive(archive);
|
|
42
|
+
expect(extracted.metadata.version).toBe(SpaceArchiveVersion.V1);
|
|
43
|
+
expect(extracted.metadata.originalSpaceId).toBe(spaceId);
|
|
44
|
+
expect(extracted.metadata.echo?.currentRootUrl).toBe('automerge:test123');
|
|
45
|
+
expect(extracted.documents['doc1' as DocumentId]).toEqual(new Uint8Array([1, 2, 3]));
|
|
46
|
+
expect(extracted.documents['doc2' as DocumentId]).toEqual(new Uint8Array([4, 5, 6]));
|
|
47
|
+
} finally {
|
|
48
|
+
await writer.close();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('Feed Archive', () => {
|
|
54
|
+
test('writes and reads a single feed with blocks', async () => {
|
|
55
|
+
const writer = new SpaceArchiveWriter();
|
|
56
|
+
await writer.open();
|
|
57
|
+
try {
|
|
58
|
+
const spaceId = SpaceId.random();
|
|
59
|
+
await writer.begin({ spaceId });
|
|
60
|
+
await writer.setCurrentRootUrl('automerge:root');
|
|
61
|
+
|
|
62
|
+
const blocks: FeedArchiveBlock[] = [
|
|
63
|
+
{
|
|
64
|
+
actorId: 'actor1',
|
|
65
|
+
sequence: 0,
|
|
66
|
+
prevActorId: null,
|
|
67
|
+
prevSequence: null,
|
|
68
|
+
position: 0,
|
|
69
|
+
timestamp: 1000,
|
|
70
|
+
data: btoa('block0'),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
actorId: 'actor1',
|
|
74
|
+
sequence: 1,
|
|
75
|
+
prevActorId: 'actor1',
|
|
76
|
+
prevSequence: 0,
|
|
77
|
+
position: 1,
|
|
78
|
+
timestamp: 2000,
|
|
79
|
+
data: btoa('block1'),
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
await writer.writeFeed('feed-123', 'data', blocks);
|
|
84
|
+
|
|
85
|
+
const archive = await writer.finish();
|
|
86
|
+
const extracted = await extractSpaceArchive(archive);
|
|
87
|
+
|
|
88
|
+
expect(Object.keys(extracted.feeds)).toHaveLength(1);
|
|
89
|
+
expect(extracted.feeds['feed-123']).toBeDefined();
|
|
90
|
+
expect(extracted.feeds['feed-123'].metadata.id).toBe('feed-123');
|
|
91
|
+
expect(extracted.feeds['feed-123'].metadata.namespace).toBe('data');
|
|
92
|
+
expect(extracted.feeds['feed-123'].blocks).toHaveLength(2);
|
|
93
|
+
expect(extracted.feeds['feed-123'].blocks[0].actorId).toBe('actor1');
|
|
94
|
+
expect(extracted.feeds['feed-123'].blocks[0].sequence).toBe(0);
|
|
95
|
+
expect(extracted.feeds['feed-123'].blocks[1].sequence).toBe(1);
|
|
96
|
+
} finally {
|
|
97
|
+
await writer.close();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('writes and reads multiple feeds', async () => {
|
|
102
|
+
const writer = new SpaceArchiveWriter();
|
|
103
|
+
await writer.open();
|
|
104
|
+
try {
|
|
105
|
+
const spaceId = SpaceId.random();
|
|
106
|
+
await writer.begin({ spaceId });
|
|
107
|
+
await writer.setCurrentRootUrl('automerge:root');
|
|
108
|
+
|
|
109
|
+
const dataBlocks: FeedArchiveBlock[] = [
|
|
110
|
+
{
|
|
111
|
+
actorId: 'actor1',
|
|
112
|
+
sequence: 0,
|
|
113
|
+
prevActorId: null,
|
|
114
|
+
prevSequence: null,
|
|
115
|
+
position: 0,
|
|
116
|
+
timestamp: 1000,
|
|
117
|
+
data: btoa('data-block'),
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const traceBlocks: FeedArchiveBlock[] = [
|
|
122
|
+
{
|
|
123
|
+
actorId: 'actor2',
|
|
124
|
+
sequence: 0,
|
|
125
|
+
prevActorId: null,
|
|
126
|
+
prevSequence: null,
|
|
127
|
+
position: 0,
|
|
128
|
+
timestamp: 2000,
|
|
129
|
+
data: btoa('trace-block'),
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
await writer.writeFeed('feed-data', 'data', dataBlocks);
|
|
134
|
+
await writer.writeFeed('feed-trace', 'trace', traceBlocks);
|
|
135
|
+
|
|
136
|
+
const archive = await writer.finish();
|
|
137
|
+
const extracted = await extractSpaceArchive(archive);
|
|
138
|
+
|
|
139
|
+
expect(Object.keys(extracted.feeds)).toHaveLength(2);
|
|
140
|
+
expect(extracted.feeds['feed-data'].metadata.namespace).toBe('data');
|
|
141
|
+
expect(extracted.feeds['feed-trace'].metadata.namespace).toBe('trace');
|
|
142
|
+
} finally {
|
|
143
|
+
await writer.close();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('writes blocks in chunks when exceeding chunk size', async () => {
|
|
148
|
+
const writer = new SpaceArchiveWriter();
|
|
149
|
+
await writer.open();
|
|
150
|
+
try {
|
|
151
|
+
const spaceId = SpaceId.random();
|
|
152
|
+
await writer.begin({ spaceId });
|
|
153
|
+
await writer.setCurrentRootUrl('automerge:root');
|
|
154
|
+
|
|
155
|
+
const numBlocks = FEED_ARCHIVE_BLOCKS_PER_CHUNK + 50;
|
|
156
|
+
const blocks: FeedArchiveBlock[] = [];
|
|
157
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
158
|
+
blocks.push({
|
|
159
|
+
actorId: 'actor1',
|
|
160
|
+
sequence: i,
|
|
161
|
+
prevActorId: i > 0 ? 'actor1' : null,
|
|
162
|
+
prevSequence: i > 0 ? i - 1 : null,
|
|
163
|
+
position: i,
|
|
164
|
+
timestamp: 1000 + i,
|
|
165
|
+
data: btoa(`block-${i}`),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await writer.writeFeed('large-feed', 'data', blocks);
|
|
170
|
+
|
|
171
|
+
const archive = await writer.finish();
|
|
172
|
+
const extracted = await extractSpaceArchive(archive);
|
|
173
|
+
|
|
174
|
+
expect(extracted.feeds['large-feed'].blocks).toHaveLength(numBlocks);
|
|
175
|
+
expect(extracted.feeds['large-feed'].blocks[0].sequence).toBe(0);
|
|
176
|
+
expect(extracted.feeds['large-feed'].blocks[numBlocks - 1].sequence).toBe(numBlocks - 1);
|
|
177
|
+
} finally {
|
|
178
|
+
await writer.close();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('handles empty feeds', async () => {
|
|
183
|
+
const writer = new SpaceArchiveWriter();
|
|
184
|
+
await writer.open();
|
|
185
|
+
try {
|
|
186
|
+
const spaceId = SpaceId.random();
|
|
187
|
+
await writer.begin({ spaceId });
|
|
188
|
+
await writer.setCurrentRootUrl('automerge:root');
|
|
189
|
+
|
|
190
|
+
await writer.writeFeed('empty-feed', 'data', []);
|
|
191
|
+
|
|
192
|
+
const archive = await writer.finish();
|
|
193
|
+
const extracted = await extractSpaceArchive(archive);
|
|
194
|
+
|
|
195
|
+
expect(extracted.feeds['empty-feed']).toBeDefined();
|
|
196
|
+
expect(extracted.feeds['empty-feed'].metadata.id).toBe('empty-feed');
|
|
197
|
+
expect(extracted.feeds['empty-feed'].blocks).toHaveLength(0);
|
|
198
|
+
} finally {
|
|
199
|
+
await writer.close();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('preserves block data encoding', async () => {
|
|
204
|
+
const writer = new SpaceArchiveWriter();
|
|
205
|
+
await writer.open();
|
|
206
|
+
try {
|
|
207
|
+
const spaceId = SpaceId.random();
|
|
208
|
+
await writer.begin({ spaceId });
|
|
209
|
+
await writer.setCurrentRootUrl('automerge:root');
|
|
210
|
+
|
|
211
|
+
const originalData = new Uint8Array([0, 1, 2, 255, 128, 64]);
|
|
212
|
+
const base64Data = btoa(String.fromCharCode(...originalData));
|
|
213
|
+
|
|
214
|
+
const blocks: FeedArchiveBlock[] = [
|
|
215
|
+
{
|
|
216
|
+
actorId: 'actor1',
|
|
217
|
+
sequence: 0,
|
|
218
|
+
prevActorId: null,
|
|
219
|
+
prevSequence: null,
|
|
220
|
+
position: null,
|
|
221
|
+
timestamp: 1000,
|
|
222
|
+
data: base64Data,
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
await writer.writeFeed('binary-feed', 'data', blocks);
|
|
227
|
+
|
|
228
|
+
const archive = await writer.finish();
|
|
229
|
+
const extracted = await extractSpaceArchive(archive);
|
|
230
|
+
|
|
231
|
+
const extractedData = extracted.feeds['binary-feed'].blocks[0].data;
|
|
232
|
+
expect(extractedData).toBe(base64Data);
|
|
233
|
+
|
|
234
|
+
const decoded = new Uint8Array(
|
|
235
|
+
atob(extractedData)
|
|
236
|
+
.split('')
|
|
237
|
+
.map((char) => char.charCodeAt(0)),
|
|
238
|
+
);
|
|
239
|
+
expect(decoded).toEqual(originalData);
|
|
240
|
+
} finally {
|
|
241
|
+
await writer.close();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('combines documents and feeds in archive', async () => {
|
|
246
|
+
const writer = new SpaceArchiveWriter();
|
|
247
|
+
await writer.open();
|
|
248
|
+
try {
|
|
249
|
+
const spaceId = SpaceId.random();
|
|
250
|
+
await writer.begin({ spaceId });
|
|
251
|
+
await writer.setCurrentRootUrl('automerge:root');
|
|
252
|
+
|
|
253
|
+
await writer.writeDocument('doc1', new Uint8Array([1, 2, 3]));
|
|
254
|
+
await writer.writeFeed('feed1', 'data', [
|
|
255
|
+
{
|
|
256
|
+
actorId: 'actor1',
|
|
257
|
+
sequence: 0,
|
|
258
|
+
prevActorId: null,
|
|
259
|
+
prevSequence: null,
|
|
260
|
+
position: 0,
|
|
261
|
+
timestamp: 1000,
|
|
262
|
+
data: btoa('test'),
|
|
263
|
+
},
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const archive = await writer.finish();
|
|
267
|
+
const extracted = await extractSpaceArchive(archive);
|
|
268
|
+
|
|
269
|
+
expect(Object.keys(extracted.documents)).toHaveLength(1);
|
|
270
|
+
expect(Object.keys(extracted.feeds)).toHaveLength(1);
|
|
271
|
+
expect(extracted.documents['doc1' as DocumentId]).toEqual(new Uint8Array([1, 2, 3]));
|
|
272
|
+
expect(extracted.feeds['feed1'].blocks).toHaveLength(1);
|
|
273
|
+
} finally {
|
|
274
|
+
await writer.close();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('File Structure', () => {
|
|
280
|
+
test('file structure constants are correct', () => {
|
|
281
|
+
expect(SpaceArchiveFileStructure.metadata).toBe('metadata.json');
|
|
282
|
+
expect(SpaceArchiveFileStructure.documents).toBe('documents');
|
|
283
|
+
expect(SpaceArchiveFileStructure.feeds).toBe('feeds');
|
|
284
|
+
expect(SpaceArchiveFileStructure.feedMetadata).toBe('metadata.json');
|
|
285
|
+
expect(SpaceArchiveFileStructure.feedBlocksPrefix).toBe('blocks-');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('blocks per chunk constant is set', () => {
|
|
289
|
+
expect(FEED_ARCHIVE_BLOCKS_PER_CHUNK).toBe(100);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('detectSpaceArchiveFormat', () => {
|
|
294
|
+
test('detects JSON via .dx.json extension', () => {
|
|
295
|
+
const format = detectSpaceArchiveFormat({ filename: 'space.dx.json', contents: new Uint8Array() });
|
|
296
|
+
expect(format).toBe(SpaceArchive.Format.JSON);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('detects JSON via .json extension', () => {
|
|
300
|
+
const format = detectSpaceArchiveFormat({ filename: 'backup.json', contents: new Uint8Array() });
|
|
301
|
+
expect(format).toBe(SpaceArchive.Format.JSON);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('detects BINARY via .tar extension', () => {
|
|
305
|
+
const format = detectSpaceArchiveFormat({ filename: 'space.tar', contents: new Uint8Array() });
|
|
306
|
+
expect(format).toBe(SpaceArchive.Format.BINARY);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('detects BINARY via .tar.gz extension', () => {
|
|
310
|
+
const format = detectSpaceArchiveFormat({ filename: 'space.tar.gz', contents: new Uint8Array() });
|
|
311
|
+
expect(format).toBe(SpaceArchive.Format.BINARY);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('falls back to JSON via leading { byte', () => {
|
|
315
|
+
const contents = new TextEncoder().encode('{"version":1}');
|
|
316
|
+
const format = detectSpaceArchiveFormat({ filename: 'unknown', contents });
|
|
317
|
+
expect(format).toBe(SpaceArchive.Format.JSON);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('skips leading whitespace before sniffing', () => {
|
|
321
|
+
const contents = new TextEncoder().encode(' \n\t{"version":1}');
|
|
322
|
+
const format = detectSpaceArchiveFormat({ filename: 'unknown', contents });
|
|
323
|
+
expect(format).toBe(SpaceArchive.Format.JSON);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('falls back to BINARY on non-JSON bytes', () => {
|
|
327
|
+
const format = detectSpaceArchiveFormat({
|
|
328
|
+
filename: 'unknown',
|
|
329
|
+
contents: new Uint8Array([0x00, 0x01, 0x02]),
|
|
330
|
+
});
|
|
331
|
+
expect(format).toBe(SpaceArchive.Format.BINARY);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('SerializedSpace reader', () => {
|
|
336
|
+
test('parses a minimal JSON archive', () => {
|
|
337
|
+
const serialized: SerializedSpace = {
|
|
338
|
+
version: 1,
|
|
339
|
+
objects: [],
|
|
340
|
+
};
|
|
341
|
+
const contents = new TextEncoder().encode(JSON.stringify(serialized));
|
|
342
|
+
const archive: SpaceArchive = {
|
|
343
|
+
filename: 'test.dx.json',
|
|
344
|
+
contents,
|
|
345
|
+
format: SpaceArchive.Format.JSON,
|
|
346
|
+
};
|
|
347
|
+
const result = readSerializedSpaceArchive(archive);
|
|
348
|
+
expect(result.version).toBe(1);
|
|
349
|
+
expect(result.objects).toEqual([]);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('rejects archives missing required fields', () => {
|
|
353
|
+
const bogus = new TextEncoder().encode(JSON.stringify({ objects: [] }));
|
|
354
|
+
expect(() =>
|
|
355
|
+
readSerializedSpaceArchive({
|
|
356
|
+
filename: 'bad.json',
|
|
357
|
+
contents: bogus,
|
|
358
|
+
format: SpaceArchive.Format.JSON,
|
|
359
|
+
}),
|
|
360
|
+
).toThrow();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('buildDatabaseDirectoryFromObjects round-trips data and type info', () => {
|
|
364
|
+
const id = ObjectId.random();
|
|
365
|
+
const objects = [
|
|
366
|
+
{
|
|
367
|
+
id,
|
|
368
|
+
'@type': 'dxn:type:example.Thing',
|
|
369
|
+
'@meta': { keys: [] },
|
|
370
|
+
title: 'hello',
|
|
371
|
+
},
|
|
372
|
+
];
|
|
373
|
+
const directory = buildDatabaseDirectoryFromObjects(objects as any);
|
|
374
|
+
expect(directory.objects).toBeDefined();
|
|
375
|
+
const structure = directory.objects![id];
|
|
376
|
+
expect(structure).toBeDefined();
|
|
377
|
+
expect(structure.data).toEqual({ title: 'hello' });
|
|
378
|
+
expect(structure.system?.type).toEqual({ '/': 'dxn:type:example.Thing' });
|
|
379
|
+
expect(structure.system?.kind).toBe('object');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('objectStructureToObjJson emits fields in canonical order', () => {
|
|
383
|
+
const id = ObjectId.random();
|
|
384
|
+
const sourceId = ObjectId.random();
|
|
385
|
+
const targetId = ObjectId.random();
|
|
386
|
+
const parentId = ObjectId.random();
|
|
387
|
+
const obj = objectStructureToObjJson(id, {
|
|
388
|
+
data: { title: 'hello', count: 42 },
|
|
389
|
+
meta: { keys: [] },
|
|
390
|
+
system: {
|
|
391
|
+
type: { '/': 'dxn:type:example.Link' },
|
|
392
|
+
kind: 'relation',
|
|
393
|
+
source: { '/': sourceId },
|
|
394
|
+
target: { '/': targetId },
|
|
395
|
+
parent: { '/': parentId },
|
|
396
|
+
deleted: true,
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
expect(Object.keys(obj)).toEqual([
|
|
401
|
+
'id',
|
|
402
|
+
'@type',
|
|
403
|
+
'@meta',
|
|
404
|
+
'@deleted',
|
|
405
|
+
'@parent',
|
|
406
|
+
'@relationSource',
|
|
407
|
+
'@relationTarget',
|
|
408
|
+
'title',
|
|
409
|
+
'count',
|
|
410
|
+
]);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('orderObjJsonFields reorders feed queue messages with id/@type/@meta first', () => {
|
|
414
|
+
const id = ObjectId.random();
|
|
415
|
+
const message = {
|
|
416
|
+
payload: { value: 'x' },
|
|
417
|
+
timestamp: 1000,
|
|
418
|
+
id,
|
|
419
|
+
'@meta': { keys: [] },
|
|
420
|
+
'@type': 'dxn:type:example.Message',
|
|
421
|
+
} as any;
|
|
422
|
+
|
|
423
|
+
const ordered = orderObjJsonFields(message);
|
|
424
|
+
expect(Object.keys(ordered)).toEqual(['id', '@type', '@meta', 'payload', 'timestamp']);
|
|
425
|
+
expect(ordered).toEqual(message);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test('orderObjJsonFields preserves unknown @-prefixed fields between system and data', () => {
|
|
429
|
+
const id = ObjectId.random();
|
|
430
|
+
const obj = {
|
|
431
|
+
data: 1,
|
|
432
|
+
'@custom': 'extension',
|
|
433
|
+
'@type': 'dxn:type:example.Thing',
|
|
434
|
+
id,
|
|
435
|
+
} as any;
|
|
436
|
+
|
|
437
|
+
const ordered = orderObjJsonFields(obj);
|
|
438
|
+
expect(Object.keys(ordered)).toEqual(['id', '@type', '@custom', 'data']);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('buildDatabaseDirectoryFromObjects flags relations', () => {
|
|
442
|
+
const id = ObjectId.random();
|
|
443
|
+
const sourceId = ObjectId.random();
|
|
444
|
+
const targetId = ObjectId.random();
|
|
445
|
+
const objects = [
|
|
446
|
+
{
|
|
447
|
+
id,
|
|
448
|
+
'@type': 'dxn:type:example.Link',
|
|
449
|
+
'@meta': { keys: [] },
|
|
450
|
+
'@relationSource': sourceId,
|
|
451
|
+
'@relationTarget': targetId,
|
|
452
|
+
},
|
|
453
|
+
];
|
|
454
|
+
const directory = buildDatabaseDirectoryFromObjects(objects as any);
|
|
455
|
+
const structure = directory.objects![id];
|
|
456
|
+
expect(structure.system?.kind).toBe('relation');
|
|
457
|
+
expect(structure.system?.source).toEqual({ '/': sourceId });
|
|
458
|
+
expect(structure.system?.target).toEqual({ '/': targetId });
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
});
|