@dxos/echo-pipeline 0.7.2 → 0.7.3-staging.0905f03

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/lib/browser/{chunk-RRKGEIVZ.mjs → chunk-LZK5YFYE.mjs} +13 -13
  2. package/dist/lib/browser/{chunk-RRKGEIVZ.mjs.map → chunk-LZK5YFYE.mjs.map} +2 -2
  3. package/dist/lib/browser/index.mjs +56 -62
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +1 -1
  7. package/dist/lib/node/{chunk-6KAVD3GU.cjs → chunk-MACQJ2EP.cjs} +16 -16
  8. package/dist/lib/node/{chunk-6KAVD3GU.cjs.map → chunk-MACQJ2EP.cjs.map} +2 -2
  9. package/dist/lib/node/index.cjs +80 -88
  10. package/dist/lib/node/index.cjs.map +4 -4
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/testing/index.cjs +10 -10
  13. package/dist/lib/node-esm/{chunk-4QES5F4H.mjs → chunk-JIZPSASG.mjs} +13 -13
  14. package/dist/lib/node-esm/{chunk-4QES5F4H.mjs.map → chunk-JIZPSASG.mjs.map} +2 -2
  15. package/dist/lib/node-esm/index.mjs +56 -62
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +1 -1
  19. package/dist/types/src/automerge/automerge-host.d.ts +7 -1
  20. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  21. package/dist/types/src/db-host/echo-host.d.ts +3 -2
  22. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  23. package/dist/types/src/db-host/index.d.ts +0 -1
  24. package/dist/types/src/db-host/index.d.ts.map +1 -1
  25. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  26. package/dist/types/src/index.d.ts +1 -0
  27. package/dist/types/src/index.d.ts.map +1 -1
  28. package/dist/types/src/space/space.d.ts.map +1 -1
  29. package/dist/types/src/{db-host/migration.d.ts → util.d.ts} +1 -3
  30. package/dist/types/src/util.d.ts.map +1 -0
  31. package/package.json +35 -34
  32. package/src/automerge/automerge-host.ts +12 -3
  33. package/src/db-host/echo-host.ts +4 -1
  34. package/src/db-host/index.ts +0 -1
  35. package/src/edge/echo-edge-replicator.test.ts +74 -31
  36. package/src/edge/echo-edge-replicator.ts +17 -3
  37. package/src/index.ts +1 -0
  38. package/src/space/space.ts +1 -0
  39. package/src/util.ts +19 -0
  40. package/dist/types/src/db-host/migration.d.ts.map +0 -1
  41. package/src/db-host/migration.ts +0 -57
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo-pipeline",
3
- "version": "0.7.2",
3
+ "version": "0.7.3-staging.0905f03",
4
4
  "description": "ECHO database.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -37,43 +37,44 @@
37
37
  "crc-32": "^1.2.2",
38
38
  "level-transcoder": "^1.0.1",
39
39
  "lodash.isequal": "^4.5.0",
40
- "@dxos/async": "0.7.2",
41
- "@dxos/automerge": "0.7.2",
42
- "@dxos/codec-protobuf": "0.7.2",
43
- "@dxos/crypto": "0.7.2",
44
- "@dxos/credentials": "0.7.2",
45
- "@dxos/debug": "0.7.2",
46
- "@dxos/context": "0.7.2",
47
- "@dxos/echo-protocol": "0.7.2",
48
- "@dxos/echo-schema": "0.7.2",
49
- "@dxos/edge-client": "0.7.2",
50
- "@dxos/feed-store": "0.7.2",
51
- "@dxos/hypercore": "0.7.2",
52
- "@dxos/indexing": "0.7.2",
53
- "@dxos/invariant": "0.7.2",
54
- "@dxos/keyring": "0.7.2",
55
- "@dxos/kv-store": "0.7.2",
56
- "@dxos/log": "0.7.2",
57
- "@dxos/keys": "0.7.2",
58
- "@dxos/network-manager": "0.7.2",
59
- "@dxos/messaging": "0.7.2",
60
- "@dxos/node-std": "0.7.2",
61
- "@dxos/protocols": "0.7.2",
62
- "@dxos/random-access-storage": "0.7.2",
63
- "@dxos/teleport-extension-automerge-replicator": "0.7.2",
64
- "@dxos/teleport": "0.7.2",
65
- "@dxos/teleport-extension-gossip": "0.7.2",
66
- "@dxos/teleport-extension-object-sync": "0.7.2",
67
- "@dxos/teleport-extension-replicator": "0.7.2",
68
- "@dxos/timeframe": "0.7.2",
69
- "@dxos/tracing": "0.7.2",
70
- "@dxos/typings": "0.7.2",
71
- "@dxos/util": "0.7.2"
40
+ "@dxos/async": "0.7.3-staging.0905f03",
41
+ "@dxos/codec-protobuf": "0.7.3-staging.0905f03",
42
+ "@dxos/automerge": "0.7.3-staging.0905f03",
43
+ "@dxos/context": "0.7.3-staging.0905f03",
44
+ "@dxos/credentials": "0.7.3-staging.0905f03",
45
+ "@dxos/crypto": "0.7.3-staging.0905f03",
46
+ "@dxos/debug": "0.7.3-staging.0905f03",
47
+ "@dxos/echo-protocol": "0.7.3-staging.0905f03",
48
+ "@dxos/echo-schema": "0.7.3-staging.0905f03",
49
+ "@dxos/edge-client": "0.7.3-staging.0905f03",
50
+ "@dxos/feed-store": "0.7.3-staging.0905f03",
51
+ "@dxos/indexing": "0.7.3-staging.0905f03",
52
+ "@dxos/hypercore": "0.7.3-staging.0905f03",
53
+ "@dxos/keyring": "0.7.3-staging.0905f03",
54
+ "@dxos/invariant": "0.7.3-staging.0905f03",
55
+ "@dxos/kv-store": "0.7.3-staging.0905f03",
56
+ "@dxos/log": "0.7.3-staging.0905f03",
57
+ "@dxos/messaging": "0.7.3-staging.0905f03",
58
+ "@dxos/keys": "0.7.3-staging.0905f03",
59
+ "@dxos/network-manager": "0.7.3-staging.0905f03",
60
+ "@dxos/node-std": "0.7.3-staging.0905f03",
61
+ "@dxos/random-access-storage": "0.7.3-staging.0905f03",
62
+ "@dxos/protocols": "0.7.3-staging.0905f03",
63
+ "@dxos/teleport": "0.7.3-staging.0905f03",
64
+ "@dxos/teleport-extension-automerge-replicator": "0.7.3-staging.0905f03",
65
+ "@dxos/teleport-extension-gossip": "0.7.3-staging.0905f03",
66
+ "@dxos/teleport-extension-object-sync": "0.7.3-staging.0905f03",
67
+ "@dxos/tracing": "0.7.3-staging.0905f03",
68
+ "@dxos/timeframe": "0.7.3-staging.0905f03",
69
+ "@dxos/teleport-extension-replicator": "0.7.3-staging.0905f03",
70
+ "@dxos/typings": "0.7.3-staging.0905f03",
71
+ "@dxos/util": "0.7.3-staging.0905f03"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/lodash.isequal": "^4.5.0",
75
75
  "fast-check": "^3.19.0",
76
- "@dxos/test-utils": "0.7.2"
76
+ "get-port-please": "^3.1.1",
77
+ "@dxos/test-utils": "0.7.3-staging.0905f03"
77
78
  },
78
79
  "publishConfig": {
79
80
  "access": "public"
@@ -42,11 +42,18 @@ import { type EchoReplicator, type RemoteDocumentExistenceCheckParams } from './
42
42
  import { HeadsStore } from './heads-store';
43
43
  import { LevelDBStorageAdapter, type BeforeSaveParams } from './leveldb-storage-adapter';
44
44
 
45
+ export type PeerIdProvider = () => string | undefined;
46
+
45
47
  export type AutomergeHostParams = {
46
48
  db: LevelDB;
47
49
 
48
50
  indexMetadataStore: IndexMetadataStore;
49
51
  dataMonitor?: EchoDataMonitor;
52
+
53
+ /**
54
+ * Used for creating stable ids. A random key is generated on open, if no value is provided.
55
+ */
56
+ peerIdProvider?: PeerIdProvider;
50
57
  };
51
58
 
52
59
  export type LoadDocOptions = {
@@ -82,9 +89,11 @@ export class AutomergeHost extends Resource {
82
89
  @trace.info()
83
90
  private _peerId!: PeerId;
84
91
 
92
+ private readonly _peerIdProvider?: PeerIdProvider;
93
+
85
94
  public readonly collectionStateUpdated = new Event<{ collectionId: CollectionId }>();
86
95
 
87
- constructor({ db, indexMetadataStore, dataMonitor }: AutomergeHostParams) {
96
+ constructor({ db, indexMetadataStore, dataMonitor, peerIdProvider }: AutomergeHostParams) {
88
97
  super();
89
98
  this._db = db;
90
99
  this._storage = new LevelDBStorageAdapter({
@@ -104,11 +113,11 @@ export class AutomergeHost extends Resource {
104
113
  });
105
114
  this._headsStore = new HeadsStore({ db: db.sublevel('heads') });
106
115
  this._indexMetadataStore = indexMetadataStore;
116
+ this._peerIdProvider = peerIdProvider;
107
117
  }
108
118
 
109
119
  protected override async _open() {
110
- // TODO(burdon): Should this be stable?
111
- this._peerId = `host-${PublicKey.random().toHex()}` as PeerId;
120
+ this._peerId = `host-${this._peerIdProvider?.() ?? PublicKey.random().toHex()}` as PeerId;
112
121
 
113
122
  await this._storage.open?.();
114
123
 
@@ -33,6 +33,7 @@ import {
33
33
  type EchoReplicator,
34
34
  type CollectionSyncState,
35
35
  type EchoDataStats,
36
+ type PeerIdProvider,
36
37
  } from '../automerge';
37
38
 
38
39
  const INDEXER_CONFIG: IndexConfig = {
@@ -42,6 +43,7 @@ const INDEXER_CONFIG: IndexConfig = {
42
43
 
43
44
  export type EchoHostParams = {
44
45
  kv: LevelDB;
46
+ peerIdProvider?: PeerIdProvider;
45
47
  };
46
48
 
47
49
  /**
@@ -59,7 +61,7 @@ export class EchoHost extends Resource {
59
61
  private readonly _spaceStateManager = new SpaceStateManager();
60
62
  private readonly _echoDataMonitor: EchoDataMonitor;
61
63
 
62
- constructor({ kv }: EchoHostParams) {
64
+ constructor({ kv, peerIdProvider }: EchoHostParams) {
63
65
  super();
64
66
 
65
67
  this._indexMetadataStore = new IndexMetadataStore({ db: kv.sublevel('index-metadata') });
@@ -70,6 +72,7 @@ export class EchoHost extends Resource {
70
72
  db: kv,
71
73
  dataMonitor: this._echoDataMonitor,
72
74
  indexMetadataStore: this._indexMetadataStore,
75
+ peerIdProvider,
73
76
  });
74
77
 
75
78
  this._indexer = new Indexer({
@@ -5,7 +5,6 @@
5
5
  export * from './data-service';
6
6
  export * from './documents-synchronizer';
7
7
  export * from './echo-host';
8
- export * from './migration';
9
8
  export * from './database-root';
10
9
  export * from './query-state';
11
10
  export * from './query-service';
@@ -2,7 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { describe, test } from 'vitest';
5
+ import { getRandomPort } from 'get-port-please';
6
+ import { describe, expect, onTestFinished, test } from 'vitest';
6
7
 
7
8
  import { Event } from '@dxos/async';
8
9
  import { cbor } from '@dxos/automerge/automerge-repo';
@@ -16,69 +17,111 @@ import type { Peer } from '@dxos/protocols/proto/dxos/edge/messenger';
16
17
  import { openAndClose } from '@dxos/test-utils';
17
18
 
18
19
  import { EchoEdgeReplicator } from './echo-edge-replicator';
19
- import type { EchoReplicatorContext } from '../automerge';
20
+ import type { EchoReplicatorContext, ReplicatorConnection } from '../automerge';
20
21
 
21
22
  describe('EchoEdgeReplicator', () => {
22
- test('reconnects', async ({ onTestFinished }) => {
23
- const { endpoint, cleanup, sendMessage } = await createTestEdgeWsServer(8001);
24
- onTestFinished(cleanup);
25
- const client = new EdgeClient(await createEphemeralEdgeIdentity(), { socketEndpoint: endpoint });
26
- await openAndClose(client);
23
+ test('reconnects', async () => {
24
+ const { client, server } = await createClientServer();
27
25
 
28
26
  const spaceId = SpaceId.random();
29
27
 
30
- const replicator = new EchoEdgeReplicator({ edgeConnection: client });
31
28
  const { context, connectionOpen } = createMockContext();
32
- await replicator.connect(context);
29
+ const replicator = await connectReplicator(client, context);
33
30
  await replicator.connectToSpace(spaceId);
34
31
 
35
32
  client.setIdentity(await createEphemeralEdgeIdentity());
36
33
  await connectionOpen.waitForCount(1);
37
34
 
38
- sendMessage(
39
- createForbiddenMessage(
40
- {
41
- identityKey: client.identityKey,
42
- peerKey: client.peerKey,
43
- },
44
- spaceId,
45
- ),
46
- );
35
+ const forbidden = createForbiddenMessage({ identityKey: client.identityKey, peerKey: client.peerKey }, spaceId);
36
+ server.sendMessage(forbidden);
47
37
  await connectionOpen.waitForCount(1);
48
38
 
49
39
  // Double restart to check for race conditions.
50
40
  client.setIdentity(await createEphemeralEdgeIdentity());
51
- sendMessage(
52
- createForbiddenMessage(
53
- {
54
- identityKey: client.identityKey,
55
- peerKey: client.peerKey,
56
- },
57
- spaceId,
58
- ),
59
- );
41
+ server.sendMessage(forbidden);
60
42
  await connectionOpen.waitForCount(1);
61
43
 
62
44
  await replicator.disconnect();
63
45
  });
46
+
47
+ describe('shouldAdvertise', () => {
48
+ test('true if space document belongs to connection space', async () => {
49
+ const { client } = await createClientServer();
50
+
51
+ const spaceId = SpaceId.random();
52
+ const documentId = PublicKey.random().toHex();
53
+ const { context, openConnections } = createMockContext({
54
+ documentSpaceId: { [documentId]: spaceId },
55
+ });
56
+ const replicator = await connectReplicator(client, context);
57
+ await replicator.connectToSpace(spaceId);
58
+
59
+ await expect.poll(() => openConnections.length === 1).toBeTruthy();
60
+ expect(openConnections[0].shouldAdvertise({ documentId })).toBeTruthy();
61
+ });
62
+
63
+ test('checks remote collection if space id can not be resolved', async () => {
64
+ const { client } = await createClientServer();
65
+
66
+ const spaceId = SpaceId.random();
67
+ const documentId = PublicKey.random().toHex();
68
+ const remoteCollections: { [peerId: string]: { [documentId: string]: boolean } } = {};
69
+ const { context, openConnections } = createMockContext({ remoteCollections });
70
+ const replicator = await connectReplicator(client, context);
71
+ await replicator.connectToSpace(spaceId);
72
+
73
+ await expect.poll(() => openConnections.length === 1).toBeTruthy();
74
+ const connection = openConnections[0];
75
+ expect(await connection.shouldAdvertise({ documentId })).toBeFalsy();
76
+ remoteCollections[connection.peerId] = { [documentId]: true };
77
+ expect(await connection.shouldAdvertise({ documentId })).toBeTruthy();
78
+ });
79
+ });
80
+
81
+ const connectReplicator = async (client: EdgeClient, context: EchoReplicatorContext) => {
82
+ const replicator = new EchoEdgeReplicator({ edgeConnection: client });
83
+ await replicator.connect(context);
84
+ onTestFinished(() => replicator.disconnect());
85
+ return replicator;
86
+ };
87
+
88
+ const createClientServer = async () => {
89
+ const server = await createTestEdgeWsServer(await getRandomPort());
90
+ onTestFinished(server.cleanup);
91
+ const client = new EdgeClient(await createEphemeralEdgeIdentity(), { socketEndpoint: server.endpoint });
92
+ await openAndClose(client);
93
+ return { client, server };
94
+ };
64
95
  });
65
96
 
66
- const createMockContext = () => {
97
+ const createMockContext = (args?: {
98
+ remoteCollections?: { [peerId: string]: { [documentId: string]: boolean } };
99
+ documentSpaceId?: { [documentId: string]: SpaceId };
100
+ }) => {
67
101
  const connectionOpen = new Event();
102
+ const openConnections: ReplicatorConnection[] = [];
68
103
  return {
69
104
  context: {
70
- getContainingSpaceIdForDocument: async (documentId) => null,
105
+ getContainingSpaceIdForDocument: async (documentId) => args?.documentSpaceId?.[documentId] ?? null,
71
106
  getContainingSpaceForDocument: async (documentId) => null,
72
107
  onConnectionAuthScopeChanged: (connection) => {},
73
- isDocumentInRemoteCollection: async (params) => false,
74
- onConnectionClosed: (connection) => {},
108
+ isDocumentInRemoteCollection: async (params) =>
109
+ args?.remoteCollections?.[params.peerId]?.[params.documentId] ?? false,
110
+ onConnectionClosed: (connection) => {
111
+ const idx = openConnections.indexOf(connection);
112
+ if (idx >= 0) {
113
+ openConnections.splice(idx, 1);
114
+ }
115
+ },
75
116
  onConnectionOpen: (connection) => {
117
+ openConnections.push(connection);
76
118
  connectionOpen.emit();
77
119
  },
78
120
 
79
121
  peerId: PublicKey.random().toHex(),
80
122
  } satisfies EchoReplicatorContext,
81
123
 
124
+ openConnections,
82
125
  connectionOpen,
83
126
  };
84
127
  };
@@ -97,6 +97,9 @@ export class EchoEdgeReplicator implements EchoReplicator {
97
97
  async connectToSpace(spaceId: SpaceId) {
98
98
  using _guard = await this._mutex.acquire();
99
99
 
100
+ if (this._connectedSpaces.has(spaceId)) {
101
+ return;
102
+ }
100
103
  this._connectedSpaces.add(spaceId);
101
104
 
102
105
  // Check if AM-repo requested that we connect to remote peers.
@@ -264,9 +267,20 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
264
267
  }
265
268
  const spaceId = await this._context.getContainingSpaceIdForDocument(params.documentId);
266
269
  if (!spaceId) {
267
- // There's no spaceId if the document is not present locally. This means the sharePolicy check is being
268
- // performed on message reception, so spaceId check was already performed in _onMessage.
269
- return true;
270
+ const remoteDocumentExists = await this._context.isDocumentInRemoteCollection({
271
+ documentId: params.documentId,
272
+ peerId: this._remotePeerId as PeerId,
273
+ });
274
+
275
+ log.info('document not found locally for share policy check, accepting the remote document', {
276
+ documentId: params.documentId,
277
+ remoteDocumentExists,
278
+ });
279
+
280
+ // If a document is not present locally return true only if it already exists on edge.
281
+ // Simply returning true will add edge to "generous peers list" for this document which will
282
+ // start replication of the document after we receive it potentially pushing it to replicator of the wrong space.
283
+ return remoteDocumentExists;
270
284
  }
271
285
  return spaceId === this._spaceId;
272
286
  }
package/src/index.ts CHANGED
@@ -9,3 +9,4 @@ export * from './pipeline';
9
9
  export * from './space';
10
10
  export * from './automerge';
11
11
  export * from './edge';
12
+ export * from './util';
@@ -48,6 +48,7 @@ export type CreatePipelineParams = {
48
48
  * Spaces are globally addressable databases with access control.
49
49
  */
50
50
  // TODO(dmaretskyi): Extract database stuff.
51
+ // TODO(dmaretskyi): Rename HaloGraph move to HALO.
51
52
  @trackLeaks('open', 'close')
52
53
  @trace.resource()
53
54
  export class Space extends Resource {
package/src/util.ts ADDED
@@ -0,0 +1,19 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { decodeReference, type ObjectStructure, type SpaceDoc } from '@dxos/echo-protocol';
6
+
7
+ /**
8
+ * Assumes properties are at root.
9
+ */
10
+ export const findInlineObjectOfType = (spaceDoc: SpaceDoc, typename: string): [string, ObjectStructure] | undefined => {
11
+ for (const id in spaceDoc.objects ?? {}) {
12
+ const obj = spaceDoc.objects![id];
13
+ if (obj.system.type && decodeReference(obj.system.type).objectId === typename) {
14
+ return [id, obj];
15
+ }
16
+ }
17
+
18
+ return undefined;
19
+ };
@@ -1 +0,0 @@
1
- {"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../../../../src/db-host/migration.ts"],"names":[],"mappings":"AAKA,OAAO,EAKL,KAAK,eAAe,EAEpB,KAAK,QAAQ,EAEd,MAAM,qBAAqB,CAAC;AAI7B,eAAO,MAAM,uBAAuB,QAAe,QAAQ,KAAG,OAAO,CAAC,QAAQ,CAU7E,CAAC;AAEF,eAAO,MAAM,yBAAyB,SAAgB,QAAQ,KAAG,OAAO,CAAC,QAAQ,CAYhF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,aAAc,QAAQ,YAAY,MAAM,KAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,SASzG,CAAC"}
@@ -1,57 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { convertLegacyReference } from '@dxos/echo-protocol';
6
- import {
7
- decodeReference,
8
- encodeReference,
9
- isLegacyReference,
10
- LEGACY_TYPE_PROPERTIES,
11
- type ObjectStructure,
12
- Reference,
13
- type SpaceDoc,
14
- SpaceDocVersion,
15
- } from '@dxos/echo-protocol';
16
- import { TYPE_PROPERTIES } from '@dxos/echo-schema';
17
- import { deepMapValuesAsync } from '@dxos/util';
18
-
19
- export const convertLegacyReferences = async (doc: SpaceDoc): Promise<SpaceDoc> => {
20
- const newDoc = await deepMapValuesAsync(doc, async (value, recurse) => {
21
- if (isLegacyReference(value)) {
22
- return convertLegacyReference(value);
23
- }
24
- return recurse(value);
25
- });
26
-
27
- newDoc.version = SpaceDocVersion.CURRENT;
28
- return newDoc;
29
- };
30
-
31
- export const convertLegacySpaceRootDoc = async (root: SpaceDoc): Promise<SpaceDoc> => {
32
- // Convert references.
33
- const newDoc: SpaceDoc = await convertLegacyReferences(root);
34
-
35
- // Update properties type.
36
- const properties = findInlineObjectOfType(newDoc, LEGACY_TYPE_PROPERTIES);
37
- if (properties) {
38
- const [_, obj] = properties;
39
- obj.system.type = encodeReference(Reference.fromLegacyTypename(TYPE_PROPERTIES));
40
- }
41
-
42
- return newDoc;
43
- };
44
-
45
- /**
46
- * Assumes properties are at root.
47
- */
48
- export const findInlineObjectOfType = (spaceDoc: SpaceDoc, typename: string): [string, ObjectStructure] | undefined => {
49
- for (const id in spaceDoc.objects ?? {}) {
50
- const obj = spaceDoc.objects![id];
51
- if (obj.system.type && decodeReference(obj.system.type).objectId === typename) {
52
- return [id, obj];
53
- }
54
- }
55
-
56
- return undefined;
57
- };