@dxos/client-services 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef

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 (163) hide show
  1. package/dist/lib/browser/{chunk-CK3KJB3B.mjs → chunk-KW4WMU5R.mjs} +1310 -3084
  2. package/dist/lib/browser/chunk-KW4WMU5R.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-NQSC7HOE.mjs → chunk-XJRPB3GA.mjs} +1 -1
  4. package/dist/lib/browser/index.mjs +100 -197
  5. package/dist/lib/browser/index.mjs.map +3 -3
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/packlets/diagnostics/browser-diagnostics-broadcast.mjs +1 -6
  8. package/dist/lib/browser/packlets/diagnostics/browser-diagnostics-broadcast.mjs.map +2 -2
  9. package/dist/lib/browser/packlets/diagnostics/diagnostics-broadcast.mjs +1 -1
  10. package/dist/lib/browser/packlets/locks/browser.mjs +9 -49
  11. package/dist/lib/browser/packlets/locks/browser.mjs.map +2 -2
  12. package/dist/lib/browser/packlets/locks/node.mjs +4 -22
  13. package/dist/lib/browser/packlets/locks/node.mjs.map +2 -2
  14. package/dist/lib/browser/testing/index.mjs +7 -27
  15. package/dist/lib/browser/testing/index.mjs.map +2 -2
  16. package/dist/lib/node-esm/{chunk-PKEGMOQ4.mjs → chunk-2DT3MZRL.mjs} +1 -1
  17. package/dist/lib/node-esm/{chunk-WHBWCIEN.mjs → chunk-NDMKP2CH.mjs} +1310 -3084
  18. package/dist/lib/node-esm/chunk-NDMKP2CH.mjs.map +7 -0
  19. package/dist/lib/node-esm/index.mjs +100 -197
  20. package/dist/lib/node-esm/index.mjs.map +3 -3
  21. package/dist/lib/node-esm/meta.json +1 -1
  22. package/dist/lib/node-esm/packlets/diagnostics/browser-diagnostics-broadcast.mjs +1 -6
  23. package/dist/lib/node-esm/packlets/diagnostics/browser-diagnostics-broadcast.mjs.map +2 -2
  24. package/dist/lib/node-esm/packlets/diagnostics/diagnostics-broadcast.mjs +1 -1
  25. package/dist/lib/node-esm/packlets/locks/browser.mjs +9 -49
  26. package/dist/lib/node-esm/packlets/locks/browser.mjs.map +2 -2
  27. package/dist/lib/node-esm/packlets/locks/node.mjs +4 -22
  28. package/dist/lib/node-esm/packlets/locks/node.mjs.map +2 -2
  29. package/dist/lib/node-esm/testing/index.mjs +7 -27
  30. package/dist/lib/node-esm/testing/index.mjs.map +2 -2
  31. package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +1 -1
  32. package/dist/types/src/packlets/agents/edge-agent-service.d.ts +2 -1
  33. package/dist/types/src/packlets/agents/edge-agent-service.d.ts.map +1 -1
  34. package/dist/types/src/packlets/devices/devices-service.d.ts.map +1 -1
  35. package/dist/types/src/packlets/devtools/devtools.d.ts.map +1 -1
  36. package/dist/types/src/packlets/devtools/feeds.d.ts.map +1 -1
  37. package/dist/types/src/packlets/devtools/keys.d.ts.map +1 -1
  38. package/dist/types/src/packlets/devtools/metadata.d.ts.map +1 -1
  39. package/dist/types/src/packlets/devtools/network.d.ts.map +1 -1
  40. package/dist/types/src/packlets/devtools/spaces.d.ts.map +1 -1
  41. package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts.map +1 -1
  42. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
  43. package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts.map +1 -1
  44. package/dist/types/src/packlets/diagnostics/diagnostics.d.ts +2 -3
  45. package/dist/types/src/packlets/diagnostics/diagnostics.d.ts.map +1 -1
  46. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  47. package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
  48. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  49. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts +2 -2
  50. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts.map +1 -1
  51. package/dist/types/src/packlets/identity/identity-service.d.ts +6 -5
  52. package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
  53. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  54. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +2 -1
  55. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  56. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +1 -1
  57. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -1
  58. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
  59. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
  60. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +5 -1
  61. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
  62. package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +1 -1
  63. package/dist/types/src/packlets/invitations/invitation-topology.d.ts.map +1 -1
  64. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  65. package/dist/types/src/packlets/invitations/invitations-manager.d.ts +1 -1
  66. package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -1
  67. package/dist/types/src/packlets/invitations/invitations-service.d.ts +3 -3
  68. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  69. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +2 -1
  70. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  71. package/dist/types/src/packlets/invitations/utils.d.ts.map +1 -1
  72. package/dist/types/src/packlets/locks/browser.d.ts.map +1 -1
  73. package/dist/types/src/packlets/locks/node.d.ts.map +1 -1
  74. package/dist/types/src/packlets/logging/logging-service.d.ts.map +1 -1
  75. package/dist/types/src/packlets/network/network-service.d.ts +5 -4
  76. package/dist/types/src/packlets/network/network-service.d.ts.map +1 -1
  77. package/dist/types/src/packlets/services/client-rpc-server.d.ts +3 -3
  78. package/dist/types/src/packlets/services/client-rpc-server.d.ts.map +1 -1
  79. package/dist/types/src/packlets/services/feed-syncer.d.ts +1 -1
  80. package/dist/types/src/packlets/services/feed-syncer.d.ts.map +1 -1
  81. package/dist/types/src/packlets/services/service-context.d.ts +1 -2
  82. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  83. package/dist/types/src/packlets/services/service-host.d.ts +1 -2
  84. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  85. package/dist/types/src/packlets/services/service-registry.d.ts.map +1 -1
  86. package/dist/types/src/packlets/services/util.d.ts.map +1 -1
  87. package/dist/types/src/packlets/space-export/archive-format.d.ts +9 -0
  88. package/dist/types/src/packlets/space-export/archive-format.d.ts.map +1 -0
  89. package/dist/types/src/packlets/space-export/index.d.ts +4 -1
  90. package/dist/types/src/packlets/space-export/index.d.ts.map +1 -1
  91. package/dist/types/src/packlets/space-export/serialized-space-reader.d.ts +23 -0
  92. package/dist/types/src/packlets/space-export/serialized-space-reader.d.ts.map +1 -0
  93. package/dist/types/src/packlets/space-export/serialized-space-writer.d.ts +36 -0
  94. package/dist/types/src/packlets/space-export/serialized-space-writer.d.ts.map +1 -0
  95. package/dist/types/src/packlets/space-export/space-archive-reader.d.ts.map +1 -1
  96. package/dist/types/src/packlets/space-export/space-archive-writer.d.ts +1 -1
  97. package/dist/types/src/packlets/space-export/space-archive-writer.d.ts.map +1 -1
  98. package/dist/types/src/packlets/spaces/automerge-space-state.d.ts.map +1 -1
  99. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +1 -2
  100. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  101. package/dist/types/src/packlets/spaces/data-space.d.ts +2 -1
  102. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  103. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  104. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
  105. package/dist/types/src/packlets/spaces/genesis.d.ts.map +1 -1
  106. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +1 -4
  107. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  108. package/dist/types/src/packlets/spaces/spaces-service.d.ts +9 -6
  109. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  110. package/dist/types/src/packlets/storage/level.d.ts.map +1 -1
  111. package/dist/types/src/packlets/storage/profile-archive.d.ts.map +1 -1
  112. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  113. package/dist/types/src/packlets/storage/util.d.ts.map +1 -1
  114. package/dist/types/src/packlets/system/system-service.d.ts +1 -1
  115. package/dist/types/src/packlets/system/system-service.d.ts.map +1 -1
  116. package/dist/types/src/packlets/testing/credential-utils.d.ts.map +1 -1
  117. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  118. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  119. package/dist/types/src/packlets/worker/worker-runtime.d.ts +11 -1
  120. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  121. package/dist/types/src/packlets/worker/worker-session.d.ts +0 -2
  122. package/dist/types/src/packlets/worker/worker-session.d.ts.map +1 -1
  123. package/dist/types/src/testing/setup.d.ts.map +1 -1
  124. package/dist/types/src/version.d.ts +1 -1
  125. package/dist/types/tsconfig.tsbuildinfo +1 -1
  126. package/package.json +39 -46
  127. package/src/packlets/agents/edge-agent-service.ts +3 -2
  128. package/src/packlets/diagnostics/diagnostics.ts +1 -2
  129. package/src/packlets/identity/identity-manager.ts +2 -4
  130. package/src/packlets/identity/identity-service.ts +12 -9
  131. package/src/packlets/identity/identity.ts +2 -2
  132. package/src/packlets/invitations/device-invitation-protocol.ts +3 -1
  133. package/src/packlets/invitations/edge-invitation-handler.ts +5 -2
  134. package/src/packlets/invitations/invitation-host-extension.ts +7 -10
  135. package/src/packlets/invitations/invitation-protocol.ts +5 -1
  136. package/src/packlets/invitations/invitation-state.ts +1 -15
  137. package/src/packlets/invitations/invitations-handler.ts +64 -12
  138. package/src/packlets/invitations/invitations-manager.ts +6 -4
  139. package/src/packlets/invitations/invitations-service.ts +6 -6
  140. package/src/packlets/invitations/space-invitation-protocol.ts +3 -3
  141. package/src/packlets/logging/logging-service.ts +15 -15
  142. package/src/packlets/network/network-service.ts +9 -8
  143. package/src/packlets/services/client-rpc-server.ts +15 -12
  144. package/src/packlets/services/service-context.ts +12 -15
  145. package/src/packlets/services/service-host.ts +8 -19
  146. package/src/packlets/space-export/archive-format.ts +42 -0
  147. package/src/packlets/space-export/index.ts +4 -1
  148. package/src/packlets/space-export/serialized-space-reader.ts +111 -0
  149. package/src/packlets/space-export/serialized-space-writer.ts +253 -0
  150. package/src/packlets/space-export/space-archive-writer.ts +2 -1
  151. package/src/packlets/space-export/space-archive.test.ts +175 -1
  152. package/src/packlets/spaces/data-space-manager.ts +17 -19
  153. package/src/packlets/spaces/data-space.ts +9 -7
  154. package/src/packlets/spaces/edge-feed-replicator.ts +1 -0
  155. package/src/packlets/spaces/spaces-service.test.ts +9 -4
  156. package/src/packlets/spaces/spaces-service.ts +91 -16
  157. package/src/packlets/worker/worker-runtime.ts +38 -4
  158. package/src/packlets/worker/worker-session.ts +4 -10
  159. package/src/version.ts +1 -1
  160. package/dist/lib/browser/chunk-CK3KJB3B.mjs.map +0 -7
  161. package/dist/lib/node-esm/chunk-WHBWCIEN.mjs.map +0 -7
  162. /package/dist/lib/browser/{chunk-NQSC7HOE.mjs.map → chunk-XJRPB3GA.mjs.map} +0 -0
  163. /package/dist/lib/node-esm/{chunk-PKEGMOQ4.mjs.map → chunk-2DT3MZRL.mjs.map} +0 -0
@@ -0,0 +1,253 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type AutomergeUrl } from '@automerge/automerge-repo';
6
+
7
+ import { Context } from '@dxos/context';
8
+ import { type Obj } from '@dxos/echo';
9
+ import { type SerializedFeed, type SerializedSpace } from '@dxos/echo-db';
10
+ import { type EchoHost } from '@dxos/echo-pipeline';
11
+ import { type DatabaseDirectory, type ObjectStructure } from '@dxos/echo-protocol';
12
+ import { assertState, invariant } from '@dxos/invariant';
13
+ import { DXN, type IdentityDid, type SpaceId } from '@dxos/keys';
14
+ import { log } from '@dxos/log';
15
+ import { FeedProtocol } from '@dxos/protocols';
16
+ import { SpaceArchive } from '@dxos/protocols/proto/dxos/client/services';
17
+ import { createFilename } from '@dxos/util';
18
+
19
+ import { type DataSpace } from '../spaces/data-space';
20
+
21
+ const SERIALIZED_SPACE_VERSION = 1;
22
+
23
+ const FEED_TYPENAME = 'org.dxos.type.feed';
24
+
25
+ const ATTR_ID = 'id';
26
+ const ATTR_TYPE = '@type';
27
+ const ATTR_META = '@meta';
28
+ const ATTR_DELETED = '@deleted';
29
+ const ATTR_PARENT = '@parent';
30
+ const ATTR_RELATION_SOURCE = '@relationSource';
31
+ const ATTR_RELATION_TARGET = '@relationTarget';
32
+
33
+ /**
34
+ * Canonical order of well-known system fields in a serialized object.
35
+ * All remaining `@*` fields follow in the order they appear on the source
36
+ * object, and finally the data fields are appended in their existing order.
37
+ */
38
+ const SYSTEM_FIELD_ORDER: readonly string[] = [
39
+ ATTR_ID,
40
+ ATTR_TYPE,
41
+ ATTR_META,
42
+ ATTR_DELETED,
43
+ ATTR_PARENT,
44
+ ATTR_RELATION_SOURCE,
45
+ ATTR_RELATION_TARGET,
46
+ ];
47
+
48
+ /**
49
+ * Reorder the keys of an {@link Obj.JSON} so that system fields appear first
50
+ * (`id`, `@type`, `@meta`, then any other `@*` attributes), followed by data
51
+ * fields. The returned object has identical values to the input — only the key
52
+ * iteration order changes.
53
+ */
54
+ export const orderObjJsonFields = (obj: Obj.JSON): Obj.JSON => {
55
+ const source = obj as Record<string, unknown>;
56
+ const result: Record<string, unknown> = {};
57
+ for (const key of SYSTEM_FIELD_ORDER) {
58
+ if (key in source) {
59
+ result[key] = source[key];
60
+ }
61
+ }
62
+ for (const key of Object.keys(source)) {
63
+ if (key.startsWith('@') && !(key in result)) {
64
+ result[key] = source[key];
65
+ }
66
+ }
67
+ for (const key of Object.keys(source)) {
68
+ if (!(key in result)) {
69
+ result[key] = source[key];
70
+ }
71
+ }
72
+ return result as Obj.JSON;
73
+ };
74
+
75
+ export type WriteSerializedSpaceArchiveOptions = {
76
+ space: DataSpace;
77
+ echoHost: EchoHost;
78
+ exportedBy?: IdentityDid;
79
+ };
80
+
81
+ /**
82
+ * Write a JSON space archive from a live {@link DataSpace}.
83
+ *
84
+ * This runs entirely inside the worker and walks automerge documents directly —
85
+ * it does not require a client-side {@link EchoDatabase}. The output conforms to
86
+ * {@link SerializedSpace} and is compatible with the JSON format consumed by the
87
+ * importer in {@link readSerializedSpaceArchive}.
88
+ */
89
+ export const writeSerializedSpaceArchive = async (
90
+ options: WriteSerializedSpaceArchiveOptions,
91
+ ): Promise<SpaceArchive> => {
92
+ const { space, echoHost, exportedBy } = options;
93
+
94
+ const rootUrl = space.automergeSpaceState.lastEpoch?.subject.assertion.automergeRoot;
95
+ assertState(rootUrl, 'Space does not have a root URL');
96
+ const databaseRoot = space.databaseRoot;
97
+ assertState(databaseRoot, 'Space database root is not ready');
98
+
99
+ const rootDoc = databaseRoot.doc();
100
+ invariant(rootDoc, 'Space database root document is not loaded');
101
+
102
+ // Collect all object structures across the root doc and any linked docs.
103
+ const objects: Obj.JSON[] = [];
104
+ collectObjectsFromDoc(rootDoc, objects);
105
+
106
+ for (const linkedUrl of databaseRoot.getAllLinkedDocuments()) {
107
+ const handle = await echoHost.loadDoc<DatabaseDirectory>(Context.default(), linkedUrl as AutomergeUrl);
108
+ await handle.whenReady();
109
+ const doc = handle.doc();
110
+ if (!doc) {
111
+ log.warn('linked document did not load; skipping', { url: linkedUrl });
112
+ continue;
113
+ }
114
+ collectObjectsFromDoc(doc, objects);
115
+ }
116
+
117
+ // Export queue/feed messages for every Feed object in the space.
118
+ const feeds = await exportFeedData(space, echoHost, objects);
119
+
120
+ const serialized: SerializedSpace = {
121
+ version: SERIALIZED_SPACE_VERSION,
122
+ timestamp: new Date().toISOString(),
123
+ originalSpaceId: space.id,
124
+ exportedBy,
125
+ createdAt: Date.now(),
126
+ objects,
127
+ ...(feeds.length > 0 ? { feeds } : {}),
128
+ };
129
+
130
+ const encoded = new TextEncoder().encode(JSON.stringify(serialized));
131
+ return {
132
+ filename: createFilename({ parts: [space.id], ext: 'dx.json' }),
133
+ contents: encoded,
134
+ format: SpaceArchive.Format.JSON,
135
+ };
136
+ };
137
+
138
+ const collectObjectsFromDoc = (doc: DatabaseDirectory, out: Obj.JSON[]): void => {
139
+ const docObjects = doc.objects ?? {};
140
+ for (const [objectId, structure] of Object.entries(docObjects)) {
141
+ out.push(objectStructureToObjJson(objectId, structure));
142
+ }
143
+ };
144
+
145
+ /**
146
+ * Convert an internal {@link ObjectStructure} into an {@link Obj.JSON}.
147
+ *
148
+ * Unlike the equivalent helper used for indexing, this preserves the object's
149
+ * `@meta` section so archives produced by this writer can be round-tripped
150
+ * through {@link Obj.fromJSON}.
151
+ */
152
+ export const objectStructureToObjJson = (objectId: string, structure: ObjectStructure): Obj.JSON => {
153
+ const result: Record<string, unknown> = {
154
+ [ATTR_ID]: objectId,
155
+ [ATTR_TYPE]: (structure.system?.type?.['/'] ?? '') as any,
156
+ };
157
+
158
+ if (structure.meta) {
159
+ result[ATTR_META] = {
160
+ keys: structure.meta.keys ?? [],
161
+ ...(structure.meta.tags ? { tags: structure.meta.tags } : {}),
162
+ };
163
+ }
164
+ if (structure.system?.deleted) {
165
+ result[ATTR_DELETED] = true;
166
+ }
167
+ if (structure.system?.parent) {
168
+ result[ATTR_PARENT] = structure.system.parent['/'];
169
+ }
170
+ if (structure.system?.source) {
171
+ result[ATTR_RELATION_SOURCE] = structure.system.source['/'];
172
+ }
173
+ if (structure.system?.target) {
174
+ result[ATTR_RELATION_TARGET] = structure.system.target['/'];
175
+ }
176
+ Object.assign(result, structure.data);
177
+
178
+ return result as Obj.JSON;
179
+ };
180
+
181
+ const exportFeedData = async (space: DataSpace, echoHost: EchoHost, objects: Obj.JSON[]): Promise<SerializedFeed[]> => {
182
+ const feeds: SerializedFeed[] = [];
183
+ const spaceId: SpaceId = space.id;
184
+
185
+ for (const obj of objects) {
186
+ if (obj[ATTR_TYPE] == null) {
187
+ continue;
188
+ }
189
+
190
+ const typeDxn = DXN.tryParse(obj[ATTR_TYPE] as string);
191
+ if (typeDxn?.asTypeDXN()?.type !== FEED_TYPENAME) {
192
+ continue;
193
+ }
194
+
195
+ const namespace = (obj as any).namespace === 'trace' ? 'trace' : 'data';
196
+ const queueDxn = new DXN(DXN.kind.QUEUE, [namespace, spaceId, obj.id]);
197
+
198
+ try {
199
+ const messages = await collectQueueMessages(echoHost, queueDxn);
200
+ if (messages.length > 0) {
201
+ feeds.push({
202
+ feedObjectId: obj.id,
203
+ namespace,
204
+ messages,
205
+ });
206
+ }
207
+ } catch (err) {
208
+ log.warn('failed to export feed data', { feedObjectId: obj.id, error: err });
209
+ }
210
+ }
211
+
212
+ return feeds;
213
+ };
214
+
215
+ const collectQueueMessages = async (echoHost: EchoHost, queueDxn: DXN): Promise<Obj.JSON[]> => {
216
+ const parts = queueDxn.asQueueDXN();
217
+ invariant(parts, 'Expected a queue DXN');
218
+
219
+ const namespace =
220
+ parts.subspaceTag === 'trace' ? FeedProtocol.WellKnownNamespaces.trace : FeedProtocol.WellKnownNamespaces.data;
221
+
222
+ const messages: Obj.JSON[] = [];
223
+ let cursor: string | undefined;
224
+ while (true) {
225
+ const result = await echoHost.queuesService.queryQueue({
226
+ query: {
227
+ spaceId: parts.spaceId,
228
+ queueIds: [parts.queueId],
229
+ queuesNamespace: namespace,
230
+ after: cursor,
231
+ },
232
+ });
233
+ const batch = (result.objects ?? []).flatMap((encoded): Obj.JSON[] => {
234
+ try {
235
+ return [JSON.parse(encoded) as Obj.JSON];
236
+ } catch (err) {
237
+ log.verbose('queue object JSON parse failed; object ignored', { encoded, error: err });
238
+ return [];
239
+ }
240
+ });
241
+ if (batch.length === 0) {
242
+ break;
243
+ }
244
+ for (const message of batch) {
245
+ messages.push(orderObjJsonFields(message));
246
+ }
247
+ if (!result.nextCursor || result.nextCursor === cursor) {
248
+ break;
249
+ }
250
+ cursor = result.nextCursor;
251
+ }
252
+ return messages;
253
+ };
@@ -15,7 +15,7 @@ import {
15
15
  type SpaceArchiveMetadata,
16
16
  SpaceArchiveVersion,
17
17
  } from '@dxos/protocols';
18
- import type { SpaceArchive } from '@dxos/protocols/proto/dxos/client/services';
18
+ import { SpaceArchive } from '@dxos/protocols/proto/dxos/client/services';
19
19
  import { createFilename } from '@dxos/util';
20
20
 
21
21
  export type SpaceArchiveBeginProps = {
@@ -115,6 +115,7 @@ export class SpaceArchiveWriter extends Resource {
115
115
  return {
116
116
  filename: createFilename({ parts: [this._meta.spaceId], ext: 'tar' }),
117
117
  contents: binary,
118
+ format: SpaceArchive.Format.BINARY,
118
119
  };
119
120
  }
120
121
  }
@@ -5,14 +5,19 @@
5
5
  import type { DocumentId } from '@automerge/automerge-repo';
6
6
  import { describe, expect, test } from 'vitest';
7
7
 
8
- import { SpaceId } from '@dxos/keys';
8
+ import { type SerializedSpace } from '@dxos/echo-db';
9
+ import { ObjectId, SpaceId } from '@dxos/keys';
9
10
  import {
10
11
  FEED_ARCHIVE_BLOCKS_PER_CHUNK,
11
12
  type FeedArchiveBlock,
12
13
  SpaceArchiveFileStructure,
13
14
  SpaceArchiveVersion,
14
15
  } from '@dxos/protocols';
16
+ import { SpaceArchive } from '@dxos/protocols/proto/dxos/client/services';
15
17
 
18
+ import { detectSpaceArchiveFormat } from './archive-format';
19
+ import { buildDatabaseDirectoryFromObjects, readSerializedSpaceArchive } from './serialized-space-reader';
20
+ import { objectStructureToObjJson, orderObjJsonFields } from './serialized-space-writer';
16
21
  import { extractSpaceArchive } from './space-archive-reader';
17
22
  import { SpaceArchiveWriter } from './space-archive-writer';
18
23
 
@@ -284,4 +289,173 @@ describe('SpaceArchive', () => {
284
289
  expect(FEED_ARCHIVE_BLOCKS_PER_CHUNK).toBe(100);
285
290
  });
286
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
+ });
287
461
  });
@@ -6,7 +6,7 @@ import { type Doc } from '@automerge/automerge';
6
6
  import { type AutomergeUrl, type DocumentId, interpretAsDocumentId } from '@automerge/automerge-repo';
7
7
 
8
8
  import { Event, synchronized, trackLeaks } from '@dxos/async';
9
- import { LegacySpaceProperties, SpaceProperties } from '@dxos/client-protocol';
9
+ import { SpaceProperties } from '@dxos/client-protocol';
10
10
  import { Context, LifecycleState, Resource, cancelWithContext } from '@dxos/context';
11
11
  import {
12
12
  type CredentialSigner,
@@ -37,7 +37,7 @@ import { assertArgument, assertState, failedInvariant, invariant } from '@dxos/i
37
37
  import { type Keyring } from '@dxos/keyring';
38
38
  import { PublicKey, type SpaceId } from '@dxos/keys';
39
39
  import { log } from '@dxos/log';
40
- import { AlreadyJoinedError, trace as Trace } from '@dxos/protocols';
40
+ import { AlreadyJoinedError } from '@dxos/protocols';
41
41
  import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
42
42
  import { type Runtime } from '@dxos/protocols/proto/dxos/config';
43
43
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
@@ -138,14 +138,12 @@ export type CreateSpaceOptions = {
138
138
  };
139
139
 
140
140
  @trackLeaks('open', 'close')
141
- @trace.resource()
141
+ @trace.resource({ lifecycle: true })
142
142
  export class DataSpaceManager extends Resource {
143
143
  public readonly updated = new Event();
144
144
 
145
145
  private readonly _spaces = new ComplexMap<PublicKey, DataSpace>(PublicKey.hash);
146
146
 
147
- private readonly _instanceId = PublicKey.random().toHex();
148
-
149
147
  private readonly _spaceManager: SpaceManager;
150
148
  private readonly _metadataStore: MetadataStore;
151
149
  private readonly _keyring: Keyring;
@@ -190,10 +188,7 @@ export class DataSpaceManager extends Resource {
190
188
  await rootHandle?.whenReady();
191
189
  const rootDoc = rootHandle?.doc();
192
190
 
193
- const properties =
194
- rootDoc &&
195
- (findInlineObjectOfType(rootDoc, Type.getTypename(SpaceProperties)) ??
196
- findInlineObjectOfType(rootDoc, Type.getTypename(LegacySpaceProperties)));
191
+ const properties = rootDoc && findInlineObjectOfType(rootDoc, Type.getTypename(SpaceProperties));
197
192
 
198
193
  return {
199
194
  key: space.key.toHex(),
@@ -221,16 +216,16 @@ export class DataSpaceManager extends Resource {
221
216
  }
222
217
 
223
218
  @synchronized
224
- protected override async _open(): Promise<void> {
219
+ @trace.span({ showInBrowserTimeline: true, op: 'lifecycle' })
220
+ protected override async _open(ctx: Context): Promise<void> {
225
221
  log('open');
226
- log.trace('dxos.echo.data-space-manager.open', Trace.begin({ id: this._instanceId }));
227
222
  log('metadata loaded', { spaces: this._metadataStore.spaces.length });
228
223
 
229
224
  const spacesToActivate: DataSpace[] = [];
230
225
  await forEachAsync(this._metadataStore.spaces, async (spaceMetadata) => {
231
226
  try {
232
227
  log('load space', { spaceMetadata });
233
- const space = await this._constructSpace(this._ctx, spaceMetadata);
228
+ const space = await this._constructSpace(ctx, spaceMetadata);
234
229
  // Track spaces that were previously active for auto-activation (used in dedicated worker mode).
235
230
  if (this._runtimeProps?.autoActivateSpaces && spaceMetadata.state === SpaceState.SPACE_ACTIVE) {
236
231
  spacesToActivate.push(space);
@@ -243,14 +238,12 @@ export class DataSpaceManager extends Resource {
243
238
  // Auto-activate spaces that were previously active (used in dedicated worker mode after leader changeover).
244
239
  for (const space of spacesToActivate) {
245
240
  log('auto-activating space', { spaceKey: space.key });
246
- space.activate(this._ctx).catch((err) => {
241
+ space.activate(ctx).catch((err) => {
247
242
  log.error('Error auto-activating space', { spaceKey: space.key, err });
248
243
  });
249
244
  }
250
245
 
251
246
  this.updated.emit();
252
-
253
- log.trace('dxos.echo.data-space-manager.open', Trace.end({ id: this._instanceId }));
254
247
  }
255
248
 
256
249
  @synchronized
@@ -266,7 +259,7 @@ export class DataSpaceManager extends Resource {
266
259
  * Creates a new space writing the genesis credentials to the control feed.
267
260
  */
268
261
  @synchronized
269
- @trace.span({ showInBrowserTimeline: true })
262
+ @trace.span({ showInBrowserTimeline: true, op: 'lifecycle' })
270
263
  async createSpace(ctx: Context, options: CreateSpaceOptions = {}): Promise<DataSpace> {
271
264
  assertArgument(
272
265
  !!options.rootUrl === !!options.documents,
@@ -372,7 +365,7 @@ export class DataSpaceManager extends Resource {
372
365
  */
373
366
  // TODO(burdon): Rename join space.
374
367
  @synchronized
375
- @trace.span({ showInBrowserTimeline: true })
368
+ @trace.span({ showInBrowserTimeline: true, op: 'lifecycle' })
376
369
  async acceptSpace(ctx: Context, opts: AcceptSpaceOptions): Promise<DataSpace> {
377
370
  log('accept space', { opts });
378
371
  invariant(this._lifecycleState === LifecycleState.OPEN, 'Not open.');
@@ -390,7 +383,11 @@ export class DataSpaceManager extends Resource {
390
383
  const space = await this._constructSpace(ctx, metadata);
391
384
  await space.open(ctx);
392
385
  await this._metadataStore.addSpace(metadata);
393
- space.initializeDataPipelineAsync();
386
+ // Use DSM lifecycle ctx: the invitation accept flow disposes `ctx` as soon as
387
+ // `acceptSpace` returns (guardedState.complete -> ctx.dispose). Detached data-pipeline
388
+ // initialization must outlive the invitation flow, and its span must be parented to a
389
+ // long-lived context.
390
+ space.initializeDataPipelineAsync(this._ctx);
394
391
 
395
392
  this.updated.emit();
396
393
  return space;
@@ -578,7 +575,8 @@ export class DataSpaceManager extends Resource {
578
575
  dataSpace.postOpen.append(async () => {
579
576
  const setting = dataSpace.getEdgeReplicationSetting();
580
577
  if (!setting || setting === EdgeReplicationSetting.ENABLED) {
581
- await this._echoEdgeReplicator?.connectToSpace(ctx, dataSpace.id);
578
+ // Use lifecycle ctx: the caller ctx from _constructSpace may be disposed by the time postOpen fires.
579
+ await this._echoEdgeReplicator?.connectToSpace(this._ctx, dataSpace.id);
582
580
  } else if (this._echoEdgeReplicator) {
583
581
  log('not connecting EchoEdgeReplicator because of EdgeReplicationSetting', { spaceId: dataSpace.id });
584
582
  }
@@ -239,7 +239,7 @@ export class DataSpace {
239
239
  }
240
240
 
241
241
  @synchronized
242
- @trace.span({ showInBrowserTimeline: true })
242
+ @trace.span({ showInBrowserTimeline: true, op: 'lifecycle' })
243
243
  async open(ctx: Context): Promise<void> {
244
244
  if (this._state === SpaceState.SPACE_CLOSED) {
245
245
  await this._open(ctx);
@@ -273,7 +273,7 @@ export class DataSpace {
273
273
  }
274
274
 
275
275
  @synchronized
276
- @trace.span({ showInBrowserTimeline: true })
276
+ @trace.span({ showInBrowserTimeline: true, op: 'lifecycle' })
277
277
  async close(ctx: Context): Promise<void> {
278
278
  await this._close(ctx);
279
279
  }
@@ -316,12 +316,14 @@ export class DataSpace {
316
316
 
317
317
  /**
318
318
  * Initialize the data pipeline in a separate task.
319
+ * @param callerCtx - Trace context from the caller (e.g., activate or createSpace) for span parenting.
319
320
  */
320
- initializeDataPipelineAsync(): void {
321
+ initializeDataPipelineAsync(callerCtx?: Context): void {
322
+ const traceCtx = callerCtx ?? this._ctx;
321
323
  scheduleTask(this._ctx, async () => {
322
324
  try {
323
325
  this.metrics.pipelineInitBegin = new Date();
324
- await this.initializeDataPipeline(this._ctx);
326
+ await this.initializeDataPipeline(traceCtx);
325
327
  } catch (err) {
326
328
  if (err instanceof CancelledError || err instanceof ContextDisposedError) {
327
329
  log('data pipeline initialization cancelled', err);
@@ -339,7 +341,7 @@ export class DataSpace {
339
341
  });
340
342
  }
341
343
 
342
- @trace.span({ showInBrowserTimeline: true })
344
+ @trace.span({ showInBrowserTimeline: true, op: 'lifecycle' })
343
345
  async initializeDataPipeline(ctx: Context): Promise<void> {
344
346
  if (this._state !== SpaceState.SPACE_CONTROL_ONLY) {
345
347
  throw new SystemError({ message: 'Invalid operation' });
@@ -395,7 +397,7 @@ export class DataSpace {
395
397
  await this._callbacks.afterReady?.();
396
398
  }
397
399
 
398
- @trace.span({ showInBrowserTimeline: true })
400
+ @trace.span({ showInBrowserTimeline: true, op: 'lifecycle' })
399
401
  private async _initializeAndReadControlPipeline(ctx: Context): Promise<void> {
400
402
  await this._inner.controlPipeline.state.waitUntilReachedTargetTimeframe({
401
403
  ctx,
@@ -600,7 +602,7 @@ export class DataSpace {
600
602
 
601
603
  await this._metadataStore.setSpaceState(this.key, SpaceState.SPACE_ACTIVE);
602
604
  await this._open(ctx);
603
- this.initializeDataPipelineAsync();
605
+ this.initializeDataPipelineAsync(ctx);
604
606
  }
605
607
 
606
608
  @synchronized
@@ -290,6 +290,7 @@ export class EdgeFeedReplicator extends Resource {
290
290
 
291
291
  private _createConnectionContext(): Context {
292
292
  const connectionCtx = new Context({
293
+ parent: this._ctx,
293
294
  onError: async (err: any) => {
294
295
  if (connectionCtx !== this._connectionCtx) {
295
296
  return;
@@ -21,10 +21,15 @@ describe('SpacesService', () => {
21
21
  beforeEach(async () => {
22
22
  serviceContext = await createServiceContext();
23
23
  await serviceContext.open(new Context());
24
- spacesService = new SpacesServiceImpl(serviceContext.identityManager, serviceContext.spaceManager, async () => {
25
- await serviceContext.initialized.wait();
26
- return serviceContext.dataSpaceManager!;
27
- });
24
+ spacesService = new SpacesServiceImpl(
25
+ serviceContext.identityManager,
26
+ serviceContext.spaceManager,
27
+ serviceContext.echoHost,
28
+ async () => {
29
+ await serviceContext.initialized.wait();
30
+ return serviceContext.dataSpaceManager!;
31
+ },
32
+ );
28
33
  });
29
34
 
30
35
  afterEach(async () => {