@dxos/client-services 0.8.4-main.bc674ce → 0.8.4-main.c351d160a8
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-J33W6T4Q.mjs → chunk-SVH323CC.mjs} +688 -124
- package/dist/lib/browser/chunk-SVH323CC.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +23 -15
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +1 -1
- package/dist/lib/node-esm/{chunk-34HKLADW.mjs → chunk-XCY7TTXS.mjs} +688 -124
- package/dist/lib/node-esm/chunk-XCY7TTXS.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +23 -15
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +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/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 +1 -2
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/worker/worker-runtime.d.ts +11 -3
- package/dist/types/src/packlets/worker/worker-runtime.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 +43 -43
- package/src/packlets/logging/logging-service.ts +4 -0
- package/src/packlets/services/feed-syncer.test.ts +340 -0
- package/src/packlets/services/feed-syncer.ts +330 -0
- package/src/packlets/services/platform.ts +7 -1
- package/src/packlets/services/service-context.ts +84 -11
- package/src/packlets/services/service-host.ts +5 -3
- package/src/packlets/spaces/data-space.ts +3 -0
- package/src/packlets/worker/worker-runtime.ts +13 -5
- package/src/packlets/worker/worker-session.ts +4 -4
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-J33W6T4Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-34HKLADW.mjs.map +0 -7
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as SqlClient from '@effect/sql/SqlClient';
|
|
6
|
+
import { Encoder, decode as cborXdecode } from 'cbor-x';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import * as Schema from 'effect/Schema';
|
|
9
|
+
|
|
10
|
+
import { AsyncTask, scheduleTask } from '@dxos/async';
|
|
11
|
+
import { Resource } from '@dxos/context';
|
|
12
|
+
import { type EdgeConnection, MessageSchema } from '@dxos/edge-client';
|
|
13
|
+
import { RuntimeProvider } from '@dxos/effect';
|
|
14
|
+
import { type FeedStore, SyncClient } from '@dxos/feed';
|
|
15
|
+
import { invariant } from '@dxos/invariant';
|
|
16
|
+
import { SpaceId } from '@dxos/keys';
|
|
17
|
+
import { FeedProtocol } from '@dxos/protocols';
|
|
18
|
+
import { EdgeService } from '@dxos/protocols';
|
|
19
|
+
import { createBuf } from '@dxos/protocols/buf';
|
|
20
|
+
import { type Message as RouterMessage } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
21
|
+
import type { SqlTransaction } from '@dxos/sql-sqlite';
|
|
22
|
+
import { bufferToArray } from '@dxos/util';
|
|
23
|
+
|
|
24
|
+
const encoder = new Encoder({ tagUint8Array: false, useRecords: false });
|
|
25
|
+
|
|
26
|
+
const DEFAULT_MESSAGE_BLOCKS_LIMIT = 50;
|
|
27
|
+
const DEFAULT_SYNC_CONCURRENCY = 5;
|
|
28
|
+
const DEFAULT_POLLING_INTERVAL = 5_000;
|
|
29
|
+
const DEFAULT_POLL_REQUEST_THROTTLE_MS = 250;
|
|
30
|
+
const MAX_BLOCKING_SYNC_ITERATIONS = 100;
|
|
31
|
+
|
|
32
|
+
interface FeedSyncerOptions {
|
|
33
|
+
runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
|
|
34
|
+
feedStore: FeedStore;
|
|
35
|
+
edgeClient: EdgeConnection;
|
|
36
|
+
peerId: string;
|
|
37
|
+
getSpaceIds: () => SpaceId[];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Namespaces to sync.
|
|
41
|
+
*/
|
|
42
|
+
syncNamespaces: string[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Maximum number of blocks to sync in a single message.
|
|
46
|
+
* @default 50
|
|
47
|
+
*/
|
|
48
|
+
messageBlocksLimit?: number;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Maximum number of spaces to sync concurrently.
|
|
52
|
+
* @default 5
|
|
53
|
+
*/
|
|
54
|
+
syncConcurrency?: number;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Interval between full polls.
|
|
58
|
+
* @default 10 seconds
|
|
59
|
+
*/
|
|
60
|
+
pollingInterval?: number;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Minimum delay between externally requested best-effort polls.
|
|
64
|
+
* @default 250 ms
|
|
65
|
+
*/
|
|
66
|
+
pollRequestThrottleMs?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class FeedSyncer extends Resource {
|
|
70
|
+
readonly #syncNamespaces: string[];
|
|
71
|
+
readonly #messageBlocksLimit: number;
|
|
72
|
+
readonly #syncConcurrency: number;
|
|
73
|
+
readonly #pollingInterval: number;
|
|
74
|
+
readonly #pollRequestThrottleMs: number;
|
|
75
|
+
|
|
76
|
+
readonly #runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
|
|
77
|
+
readonly #feedStore: FeedStore;
|
|
78
|
+
readonly #edgeClient: EdgeConnection;
|
|
79
|
+
readonly #syncClient: SyncClient;
|
|
80
|
+
readonly #getSpaceIds: () => SpaceId[];
|
|
81
|
+
|
|
82
|
+
#spacesToPoll = new Set<SpaceId>();
|
|
83
|
+
/** Last time full poll was completed. */
|
|
84
|
+
#lastFullPoll: number | null = null;
|
|
85
|
+
#throttledPollScheduled = false;
|
|
86
|
+
#lastRequestedPollAt: number | null = null;
|
|
87
|
+
|
|
88
|
+
constructor(options: FeedSyncerOptions) {
|
|
89
|
+
super();
|
|
90
|
+
this.#runtime = options.runtime;
|
|
91
|
+
this.#feedStore = options.feedStore;
|
|
92
|
+
this.#edgeClient = options.edgeClient;
|
|
93
|
+
this.#syncClient = new SyncClient({
|
|
94
|
+
peerId: options.peerId,
|
|
95
|
+
feedStore: options.feedStore,
|
|
96
|
+
sendMessage: this.#sendMessage.bind(this),
|
|
97
|
+
});
|
|
98
|
+
this.#getSpaceIds = options.getSpaceIds;
|
|
99
|
+
this.#syncNamespaces = options.syncNamespaces;
|
|
100
|
+
this.#messageBlocksLimit = options.messageBlocksLimit ?? DEFAULT_MESSAGE_BLOCKS_LIMIT;
|
|
101
|
+
this.#syncConcurrency = options.syncConcurrency ?? DEFAULT_SYNC_CONCURRENCY;
|
|
102
|
+
this.#pollingInterval = options.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
103
|
+
this.#pollRequestThrottleMs = options.pollRequestThrottleMs ?? DEFAULT_POLL_REQUEST_THROTTLE_MS;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected override async _open(): Promise<void> {
|
|
107
|
+
this._ctx.onDispose(
|
|
108
|
+
this.#edgeClient.onMessage((msg: RouterMessage) => {
|
|
109
|
+
if (!msg.serviceId) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const service = msg.serviceId.split(':')[0];
|
|
113
|
+
if (service !== EdgeService.QUEUE_REPLICATOR) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const handleMessageEffect = Effect.gen(this, function* () {
|
|
117
|
+
const decoded = yield* Effect.try({
|
|
118
|
+
try: () => cborXdecode(msg.payload!.value),
|
|
119
|
+
catch: (error) => new Error(`Failed to decode feed sync message: ${error}`),
|
|
120
|
+
});
|
|
121
|
+
const payload = yield* Schema.validate(FeedProtocol.ProtocolMessage)(decoded);
|
|
122
|
+
yield* this.#syncClient.handleMessage(payload);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
void RuntimeProvider.runPromise(this.#runtime)(handleMessageEffect);
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
this._ctx.onDispose(
|
|
130
|
+
// NOTE: This will fire immediately if the connection is already open.
|
|
131
|
+
this.#edgeClient.onReconnected(async () => {}),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
this.#feedStore.onNewBlocks.on(this._ctx, () => {
|
|
135
|
+
this.#pushTask.schedule();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await this.#pollTask.open();
|
|
139
|
+
await this.#pushTask.open();
|
|
140
|
+
|
|
141
|
+
this.#resetSpacesToPoll();
|
|
142
|
+
this.#pollTask.schedule();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected override async _close(): Promise<void> {
|
|
146
|
+
await this.#pollTask.close();
|
|
147
|
+
await this.#pushTask.close();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Schedules a best-effort pull without blocking the caller.
|
|
152
|
+
*/
|
|
153
|
+
schedulePoll(): void {
|
|
154
|
+
this.#resetSpacesToPoll();
|
|
155
|
+
if (this.#throttledPollScheduled) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
const delay =
|
|
161
|
+
this.#lastRequestedPollAt == null
|
|
162
|
+
? 0
|
|
163
|
+
: Math.max(this.#pollRequestThrottleMs - (now - this.#lastRequestedPollAt), 0);
|
|
164
|
+
this.#throttledPollScheduled = true;
|
|
165
|
+
scheduleTask(
|
|
166
|
+
this._ctx,
|
|
167
|
+
() => {
|
|
168
|
+
this.#throttledPollScheduled = false;
|
|
169
|
+
this.#lastRequestedPollAt = Date.now();
|
|
170
|
+
this.#pollTask.schedule();
|
|
171
|
+
},
|
|
172
|
+
delay,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Performs queue sync and blocks until there are no pending sync batches.
|
|
178
|
+
*/
|
|
179
|
+
async syncBlocking({
|
|
180
|
+
spaceId,
|
|
181
|
+
subspaceTag,
|
|
182
|
+
shouldPush = true,
|
|
183
|
+
shouldPull = true,
|
|
184
|
+
}: {
|
|
185
|
+
spaceId: SpaceId;
|
|
186
|
+
subspaceTag: string;
|
|
187
|
+
shouldPush?: boolean;
|
|
188
|
+
shouldPull?: boolean;
|
|
189
|
+
}): Promise<void> {
|
|
190
|
+
invariant(SpaceId.isValid(spaceId));
|
|
191
|
+
invariant(FeedProtocol.isWellKnownNamespace(subspaceTag));
|
|
192
|
+
if (!shouldPush && !shouldPull) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await RuntimeProvider.runPromise(this.#runtime)(
|
|
197
|
+
Effect.gen(this, function* () {
|
|
198
|
+
let done = false;
|
|
199
|
+
let iterations = 0;
|
|
200
|
+
while (!done) {
|
|
201
|
+
done = true;
|
|
202
|
+
if (shouldPull) {
|
|
203
|
+
const pullResult = yield* this.#syncClient.pull({
|
|
204
|
+
spaceId,
|
|
205
|
+
feedNamespace: subspaceTag,
|
|
206
|
+
limit: this.#messageBlocksLimit,
|
|
207
|
+
});
|
|
208
|
+
done &&= pullResult.done;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (shouldPush) {
|
|
212
|
+
const pushResult = yield* this.#syncClient.push({
|
|
213
|
+
spaceId,
|
|
214
|
+
feedNamespace: subspaceTag,
|
|
215
|
+
limit: this.#messageBlocksLimit,
|
|
216
|
+
});
|
|
217
|
+
done &&= pushResult.done;
|
|
218
|
+
}
|
|
219
|
+
iterations++;
|
|
220
|
+
if (iterations > MAX_BLOCKING_SYNC_ITERATIONS) {
|
|
221
|
+
throw new Error('Blocking sync exceeded max iterations.');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#resetSpacesToPoll(): void {
|
|
229
|
+
this.#spacesToPoll.clear();
|
|
230
|
+
this.#getSpaceIds().forEach((spaceId) => {
|
|
231
|
+
this.#spacesToPoll.add(spaceId);
|
|
232
|
+
});
|
|
233
|
+
this.#lastFullPoll = Date.now();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#sendMessage(message: FeedProtocol.QueryRequest | FeedProtocol.AppendRequest): Effect.Effect<void, unknown, never> {
|
|
237
|
+
return Effect.gen(this, function* () {
|
|
238
|
+
const encoded = encoder.encode(message);
|
|
239
|
+
yield* Effect.tryPromise(async () =>
|
|
240
|
+
this.#edgeClient.send(
|
|
241
|
+
createBuf(MessageSchema, {
|
|
242
|
+
source: {
|
|
243
|
+
identityKey: this.#edgeClient.identityKey,
|
|
244
|
+
peerKey: this.#edgeClient.peerKey,
|
|
245
|
+
},
|
|
246
|
+
serviceId: this.#getTargetServiceId(message),
|
|
247
|
+
payload: { value: bufferToArray(encoded) },
|
|
248
|
+
}),
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#getTargetServiceId(message: FeedProtocol.QueryRequest | FeedProtocol.AppendRequest): string {
|
|
255
|
+
// TODO(dmaretskyi): Perhaps in the future we will want to include the queue namespace here as well.
|
|
256
|
+
// This would require putting it at the top level of the message.
|
|
257
|
+
// For now, we let the edge router handle it.
|
|
258
|
+
return FeedProtocol.encodeServiceId(message.feedNamespace, message.spaceId as SpaceId);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
readonly #pollTask = new AsyncTask(async () =>
|
|
262
|
+
Effect.gen(this, function* () {
|
|
263
|
+
yield* Effect.forEach(
|
|
264
|
+
this.#spacesToPoll,
|
|
265
|
+
(spaceId) =>
|
|
266
|
+
Effect.gen(this, function* () {
|
|
267
|
+
let doneForAllNamespaces = true;
|
|
268
|
+
for (const feedNamespace of this.#syncNamespaces) {
|
|
269
|
+
const { done } = yield* this.#syncClient.pull({
|
|
270
|
+
spaceId,
|
|
271
|
+
feedNamespace,
|
|
272
|
+
limit: this.#messageBlocksLimit,
|
|
273
|
+
});
|
|
274
|
+
if (!done) {
|
|
275
|
+
doneForAllNamespaces = false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (doneForAllNamespaces) {
|
|
279
|
+
this.#spacesToPoll.delete(spaceId);
|
|
280
|
+
}
|
|
281
|
+
}),
|
|
282
|
+
{ concurrency: this.#syncConcurrency },
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// If its time to do a full poll, reset the spaces to poll and schedule the next poll immediately.
|
|
286
|
+
if (this.#lastFullPoll == null || Date.now() - this.#lastFullPoll > this.#pollingInterval) {
|
|
287
|
+
this.#resetSpacesToPoll();
|
|
288
|
+
this.#pollTask.schedule();
|
|
289
|
+
} else if (this.#spacesToPoll.size > 0) {
|
|
290
|
+
// If there are some spaces still syncing, poll them immediately.
|
|
291
|
+
this.#pollTask.schedule();
|
|
292
|
+
} else {
|
|
293
|
+
// All spaces sync, and there's time before the next full poll, schedule it later.
|
|
294
|
+
this.#resetSpacesToPoll();
|
|
295
|
+
scheduleTask(
|
|
296
|
+
this._ctx,
|
|
297
|
+
() => this.#pollTask.schedule(),
|
|
298
|
+
Math.max(this.#pollingInterval - (Date.now() - (this.#lastFullPoll ?? 0)), 0),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}).pipe(RuntimeProvider.runPromise(this.#runtime)),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
readonly #pushTask = new AsyncTask(async () =>
|
|
305
|
+
Effect.gen(this, function* () {
|
|
306
|
+
yield* Effect.forEach(
|
|
307
|
+
this.#getSpaceIds(),
|
|
308
|
+
(spaceId) =>
|
|
309
|
+
Effect.gen(this, function* () {
|
|
310
|
+
let doneForAllNamespaces = true;
|
|
311
|
+
for (const feedNamespace of this.#syncNamespaces) {
|
|
312
|
+
const { done } = yield* this.#syncClient.push({
|
|
313
|
+
spaceId,
|
|
314
|
+
feedNamespace,
|
|
315
|
+
limit: this.#messageBlocksLimit,
|
|
316
|
+
});
|
|
317
|
+
if (!done) {
|
|
318
|
+
doneForAllNamespaces = false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!doneForAllNamespaces) {
|
|
322
|
+
// Keep pushing until all blocks are pushed.
|
|
323
|
+
this.#pushTask.schedule();
|
|
324
|
+
}
|
|
325
|
+
}),
|
|
326
|
+
{ concurrency: this.#syncConcurrency },
|
|
327
|
+
);
|
|
328
|
+
}).pipe(RuntimeProvider.runPromise(this.#runtime)),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
@@ -14,12 +14,18 @@ export const getPlatform = (): Platform => {
|
|
|
14
14
|
userAgent,
|
|
15
15
|
uptime: Math.floor((Date.now() - window.performance.timeOrigin) / 1_000),
|
|
16
16
|
};
|
|
17
|
-
} else {
|
|
17
|
+
} else if (typeof SharedWorkerGlobalScope !== 'undefined') {
|
|
18
18
|
// Shared worker.
|
|
19
19
|
return {
|
|
20
20
|
type: Platform.PLATFORM_TYPE.SHARED_WORKER,
|
|
21
21
|
uptime: Math.floor((Date.now() - performance.timeOrigin) / 1_000),
|
|
22
22
|
};
|
|
23
|
+
} else {
|
|
24
|
+
// Dedicated worker.
|
|
25
|
+
return {
|
|
26
|
+
type: Platform.PLATFORM_TYPE.DEDICATED_WORKER,
|
|
27
|
+
uptime: Math.floor((Date.now() - performance.timeOrigin) / 1_000),
|
|
28
|
+
};
|
|
23
29
|
}
|
|
24
30
|
} else {
|
|
25
31
|
// Node.
|
|
@@ -22,12 +22,13 @@ import { type RuntimeProvider } from '@dxos/effect';
|
|
|
22
22
|
import { FeedFactory, FeedStore } from '@dxos/feed-store';
|
|
23
23
|
import { invariant } from '@dxos/invariant';
|
|
24
24
|
import { Keyring } from '@dxos/keyring';
|
|
25
|
-
import { PublicKey } from '@dxos/keys';
|
|
25
|
+
import { PublicKey, type SpaceId } from '@dxos/keys';
|
|
26
26
|
import { type LevelDB } from '@dxos/kv-store';
|
|
27
27
|
import { log } from '@dxos/log';
|
|
28
28
|
import { type SignalManager } from '@dxos/messaging';
|
|
29
29
|
import { type SwarmNetworkManager } from '@dxos/network-manager';
|
|
30
30
|
import { InvalidStorageVersionError, STORAGE_VERSION, trace } from '@dxos/protocols';
|
|
31
|
+
import { FeedProtocol } from '@dxos/protocols';
|
|
31
32
|
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
32
33
|
import { type Runtime } from '@dxos/protocols/proto/dxos/config';
|
|
33
34
|
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
@@ -56,6 +57,8 @@ import {
|
|
|
56
57
|
} from '../invitations';
|
|
57
58
|
import { DataSpaceManager, type DataSpaceManagerRuntimeProps, type SigningContext } from '../spaces';
|
|
58
59
|
|
|
60
|
+
import { FeedSyncer } from './feed-syncer';
|
|
61
|
+
|
|
59
62
|
export type ServiceContextRuntimeProps = Pick<
|
|
60
63
|
IdentityManagerProps,
|
|
61
64
|
'devicePresenceOfflineTimeout' | 'devicePresenceAnnounceInterval'
|
|
@@ -64,8 +67,6 @@ export type ServiceContextRuntimeProps = Pick<
|
|
|
64
67
|
invitationConnectionDefaultProps?: InvitationConnectionProps;
|
|
65
68
|
disableP2pReplication?: boolean;
|
|
66
69
|
enableVectorIndexing?: boolean;
|
|
67
|
-
enableSqlite?: boolean;
|
|
68
|
-
enableLocalQueues?: boolean;
|
|
69
70
|
};
|
|
70
71
|
/**
|
|
71
72
|
* Shared backend for all client services.
|
|
@@ -90,6 +91,7 @@ export class ServiceContext extends Resource {
|
|
|
90
91
|
public readonly echoHost: EchoHost;
|
|
91
92
|
private readonly _meshReplicator?: MeshEchoReplicator = undefined;
|
|
92
93
|
private readonly _echoEdgeReplicator?: EchoEdgeReplicator = undefined;
|
|
94
|
+
private readonly _feedSyncer?: FeedSyncer = undefined;
|
|
93
95
|
|
|
94
96
|
// Initialized after identity is initialized.
|
|
95
97
|
public dataSpaceManager?: DataSpaceManager;
|
|
@@ -166,12 +168,15 @@ export class ServiceContext extends Resource {
|
|
|
166
168
|
kv: this.level,
|
|
167
169
|
peerIdProvider: () => this.identityManager.identity?.deviceKey?.toHex(),
|
|
168
170
|
getSpaceKeyByRootDocumentId: (documentId) => this.spaceManager.findSpaceByRootDocumentId(documentId)?.key,
|
|
169
|
-
indexing: {
|
|
170
|
-
vector: this._runtimeProps?.enableVectorIndexing,
|
|
171
|
-
sqlIndex: this._runtimeProps?.enableSqlite,
|
|
172
|
-
},
|
|
173
171
|
runtime: this._runtime,
|
|
174
|
-
|
|
172
|
+
syncQueue: async (request) => {
|
|
173
|
+
return this._feedSyncer?.syncBlocking({
|
|
174
|
+
spaceId: request.spaceId as SpaceId,
|
|
175
|
+
subspaceTag: request.subspaceTag,
|
|
176
|
+
shouldPush: request.shouldPush,
|
|
177
|
+
shouldPull: request.shouldPull,
|
|
178
|
+
});
|
|
179
|
+
},
|
|
175
180
|
});
|
|
176
181
|
|
|
177
182
|
this.invitations = new InvitationsHandler(
|
|
@@ -206,6 +211,17 @@ export class ServiceContext extends Resource {
|
|
|
206
211
|
edgeHttpClient: this._edgeHttpClient,
|
|
207
212
|
});
|
|
208
213
|
}
|
|
214
|
+
|
|
215
|
+
if (this.echoHost.feedStore && this._edgeConnection) {
|
|
216
|
+
this._feedSyncer = new FeedSyncer({
|
|
217
|
+
runtime: this._runtime,
|
|
218
|
+
feedStore: this.echoHost.feedStore,
|
|
219
|
+
edgeClient: this._edgeConnection,
|
|
220
|
+
peerId: this.identityManager.identity?.deviceKey?.toHex() ?? '',
|
|
221
|
+
getSpaceIds: () => this.echoHost!.spaceIds,
|
|
222
|
+
syncNamespaces: [FeedProtocol.WellKnownNamespaces.data, FeedProtocol.WellKnownNamespaces.trace],
|
|
223
|
+
});
|
|
224
|
+
}
|
|
209
225
|
}
|
|
210
226
|
|
|
211
227
|
@Trace.span()
|
|
@@ -215,31 +231,66 @@ export class ServiceContext extends Resource {
|
|
|
215
231
|
log('opening...');
|
|
216
232
|
log.trace('dxos.sdk.service-context.open', trace.begin({ id: this._instanceId }));
|
|
217
233
|
|
|
234
|
+
log('opening identityManager...');
|
|
218
235
|
await this.identityManager.open(ctx);
|
|
236
|
+
log('identityManager opened', { hasIdentity: !!this.identityManager.identity });
|
|
219
237
|
|
|
238
|
+
log('setting network identity...');
|
|
220
239
|
await this._setNetworkIdentity();
|
|
240
|
+
log('network identity set');
|
|
221
241
|
|
|
242
|
+
log('opening edge connection...');
|
|
222
243
|
await this._edgeConnection?.open();
|
|
244
|
+
log('edge connection opened');
|
|
245
|
+
|
|
246
|
+
log('opening signal manager...');
|
|
223
247
|
await this.signalManager.open();
|
|
248
|
+
log('signal manager opened');
|
|
249
|
+
|
|
250
|
+
log('opening network manager...');
|
|
224
251
|
await this.networkManager.open();
|
|
252
|
+
log('network manager opened');
|
|
225
253
|
|
|
254
|
+
log('opening echo host...');
|
|
226
255
|
await this.echoHost.open(ctx);
|
|
256
|
+
log('echo host opened');
|
|
227
257
|
|
|
228
258
|
if (this._meshReplicator) {
|
|
259
|
+
log('adding mesh replicator...');
|
|
229
260
|
await this.echoHost.addReplicator(this._meshReplicator);
|
|
261
|
+
log('mesh replicator added');
|
|
230
262
|
}
|
|
231
263
|
if (this._echoEdgeReplicator) {
|
|
264
|
+
log('adding edge replicator...');
|
|
232
265
|
await this.echoHost.addReplicator(this._echoEdgeReplicator);
|
|
266
|
+
log('edge replicator added');
|
|
233
267
|
}
|
|
234
268
|
|
|
269
|
+
log('loading metadata store...');
|
|
235
270
|
await this.metadataStore.load();
|
|
271
|
+
log('metadata store loaded');
|
|
272
|
+
|
|
273
|
+
log('opening space manager...');
|
|
236
274
|
await this.spaceManager.open();
|
|
275
|
+
log('space manager opened');
|
|
237
276
|
|
|
238
277
|
if (this.identityManager.identity) {
|
|
278
|
+
log('joining network...');
|
|
239
279
|
await this.identityManager.identity.joinNetwork();
|
|
280
|
+
log('network joined');
|
|
281
|
+
|
|
282
|
+
log('initializing spaces...(calling _initialize)');
|
|
240
283
|
await this._initialize(ctx);
|
|
284
|
+
log('spaces initialized');
|
|
285
|
+
} else {
|
|
286
|
+
log('no identity, skipping network join and space initialization');
|
|
241
287
|
}
|
|
242
288
|
|
|
289
|
+
log('opening feed syncer...');
|
|
290
|
+
await this._feedSyncer?.open();
|
|
291
|
+
log('feed syncer opened');
|
|
292
|
+
|
|
293
|
+
log('loading persistent invitations...');
|
|
243
294
|
const loadedInvitations = await this.invitationsManager.loadPersistentInvitations();
|
|
244
295
|
log('loaded persistent invitations', { count: loadedInvitations.invitations?.length });
|
|
245
296
|
|
|
@@ -249,6 +300,9 @@ export class ServiceContext extends Resource {
|
|
|
249
300
|
|
|
250
301
|
protected override async _close(ctx: Context): Promise<void> {
|
|
251
302
|
log('closing...');
|
|
303
|
+
|
|
304
|
+
await this._feedSyncer?.close();
|
|
305
|
+
|
|
252
306
|
if (this._deviceSpaceSync && this.identityManager.identity) {
|
|
253
307
|
await this.identityManager.identity.space.spaceState.removeCredentialProcessor(this._deviceSpaceSync);
|
|
254
308
|
}
|
|
@@ -314,7 +368,7 @@ export class ServiceContext extends Resource {
|
|
|
314
368
|
// Called when identity is created.
|
|
315
369
|
@Trace.span()
|
|
316
370
|
private async _initialize(ctx: Context): Promise<void> {
|
|
317
|
-
log('
|
|
371
|
+
log('_initialize: start');
|
|
318
372
|
const identity = this.identityManager.identity ?? failUndefined();
|
|
319
373
|
const signingContext: SigningContext = {
|
|
320
374
|
credentialSigner: identity.getIdentityCredentialSigner(),
|
|
@@ -326,6 +380,7 @@ export class ServiceContext extends Resource {
|
|
|
326
380
|
},
|
|
327
381
|
};
|
|
328
382
|
|
|
383
|
+
log('_initialize: creating DataSpaceManager');
|
|
329
384
|
this.dataSpaceManager = new DataSpaceManager({
|
|
330
385
|
spaceManager: this.spaceManager,
|
|
331
386
|
metadataStore: this.metadataStore,
|
|
@@ -341,7 +396,9 @@ export class ServiceContext extends Resource {
|
|
|
341
396
|
runtimeProps: this._runtimeProps as DataSpaceManagerRuntimeProps,
|
|
342
397
|
edgeFeatures: this._edgeFeatures,
|
|
343
398
|
});
|
|
399
|
+
log('_initialize: opening DataSpaceManager...');
|
|
344
400
|
await this.dataSpaceManager.open();
|
|
401
|
+
log('_initialize: DataSpaceManager opened');
|
|
345
402
|
|
|
346
403
|
this.edgeAgentManager = new EdgeAgentManager(
|
|
347
404
|
this._edgeFeatures,
|
|
@@ -349,13 +406,16 @@ export class ServiceContext extends Resource {
|
|
|
349
406
|
this.dataSpaceManager,
|
|
350
407
|
identity,
|
|
351
408
|
);
|
|
409
|
+
log('_initialize: opening EdgeAgentManager...');
|
|
352
410
|
await this.edgeAgentManager.open();
|
|
411
|
+
log('_initialize: EdgeAgentManager opened');
|
|
353
412
|
|
|
354
413
|
this._handlerFactories.set(Invitation.Kind.SPACE, (invitation) => {
|
|
355
414
|
invariant(this.dataSpaceManager, 'dataSpaceManager not initialized yet');
|
|
356
415
|
return new SpaceInvitationProtocol(this.dataSpaceManager, signingContext, this.keyring, invitation.spaceKey);
|
|
357
416
|
});
|
|
358
417
|
this.initialized.wake();
|
|
418
|
+
log('_initialize: initialized.wake() called');
|
|
359
419
|
|
|
360
420
|
this._deviceSpaceSync = {
|
|
361
421
|
processCredential: async (credential: Credential) => {
|
|
@@ -392,17 +452,20 @@ export class ServiceContext extends Resource {
|
|
|
392
452
|
}
|
|
393
453
|
|
|
394
454
|
private async _setNetworkIdentity(params?: { deviceCredential: Credential }): Promise<void> {
|
|
455
|
+
log('_setNetworkIdentity: acquiring mutex...');
|
|
395
456
|
using _ = await this._edgeIdentityUpdateMutex.acquire();
|
|
457
|
+
log('_setNetworkIdentity: mutex acquired');
|
|
396
458
|
|
|
397
459
|
let edgeIdentity: EdgeIdentity;
|
|
398
460
|
const identity = this.identityManager.identity;
|
|
399
461
|
if (identity) {
|
|
400
|
-
log('
|
|
462
|
+
log('_setNetworkIdentity: has identity', {
|
|
401
463
|
identity: identity.identityKey.toHex(),
|
|
402
|
-
|
|
464
|
+
hasDeviceCredential: !!params?.deviceCredential,
|
|
403
465
|
});
|
|
404
466
|
|
|
405
467
|
if (params?.deviceCredential) {
|
|
468
|
+
log('_setNetworkIdentity: creating chain edge identity with device credential...');
|
|
406
469
|
edgeIdentity = await createChainEdgeIdentity(
|
|
407
470
|
identity.signer,
|
|
408
471
|
identity.identityKey,
|
|
@@ -410,14 +473,20 @@ export class ServiceContext extends Resource {
|
|
|
410
473
|
params?.deviceCredential && { credential: params.deviceCredential },
|
|
411
474
|
[], // TODO(dmaretskyi): Service access credentials.
|
|
412
475
|
);
|
|
476
|
+
log('_setNetworkIdentity: chain edge identity created');
|
|
413
477
|
} else {
|
|
478
|
+
log('_setNetworkIdentity: waiting for identity.ready()...');
|
|
414
479
|
// TODO: throw here or from identity if device chain can't be loaded, to avoid indefinite hangup
|
|
415
480
|
await warnAfterTimeout(10_000, 'Waiting for identity to be ready for edge connection', async () => {
|
|
416
481
|
await identity.ready();
|
|
417
482
|
});
|
|
483
|
+
log('_setNetworkIdentity: identity.ready() resolved', {
|
|
484
|
+
hasDeviceCredentialChain: !!identity.deviceCredentialChain,
|
|
485
|
+
});
|
|
418
486
|
|
|
419
487
|
invariant(identity.deviceCredentialChain);
|
|
420
488
|
|
|
489
|
+
log('_setNetworkIdentity: creating chain edge identity...');
|
|
421
490
|
edgeIdentity = await createChainEdgeIdentity(
|
|
422
491
|
identity.signer,
|
|
423
492
|
identity.identityKey,
|
|
@@ -425,9 +494,12 @@ export class ServiceContext extends Resource {
|
|
|
425
494
|
identity.deviceCredentialChain,
|
|
426
495
|
[], // TODO(dmaretskyi): Service access credentials.
|
|
427
496
|
);
|
|
497
|
+
log('_setNetworkIdentity: chain edge identity created');
|
|
428
498
|
}
|
|
429
499
|
} else {
|
|
500
|
+
log('_setNetworkIdentity: no identity, creating ephemeral edge identity...');
|
|
430
501
|
edgeIdentity = await createEphemeralEdgeIdentity();
|
|
502
|
+
log('_setNetworkIdentity: ephemeral edge identity created');
|
|
431
503
|
}
|
|
432
504
|
|
|
433
505
|
this._edgeConnection?.setIdentity(edgeIdentity);
|
|
@@ -436,5 +508,6 @@ export class ServiceContext extends Resource {
|
|
|
436
508
|
identityKey: edgeIdentity.identityKey,
|
|
437
509
|
peerKey: edgeIdentity.peerKey,
|
|
438
510
|
});
|
|
511
|
+
log('_setNetworkIdentity: done');
|
|
439
512
|
}
|
|
440
513
|
}
|
|
@@ -244,9 +244,6 @@ export class ClientServicesHost {
|
|
|
244
244
|
if (this._runtimeProps.enableVectorIndexing === undefined) {
|
|
245
245
|
this._runtimeProps.enableVectorIndexing = config?.get('runtime.client.enableVectorIndexing', false);
|
|
246
246
|
}
|
|
247
|
-
if (this._runtimeProps.enableLocalQueues === undefined) {
|
|
248
|
-
this._runtimeProps.enableLocalQueues = config?.get('runtime.client.enableLocalQueues', false);
|
|
249
|
-
}
|
|
250
247
|
|
|
251
248
|
invariant(!this._config, 'config already set');
|
|
252
249
|
this._config = config;
|
|
@@ -395,8 +392,13 @@ export class ClientServicesHost {
|
|
|
395
392
|
EdgeAgentService: new EdgeAgentServiceImpl(agentManagerProvider, this._edgeConnection),
|
|
396
393
|
});
|
|
397
394
|
|
|
395
|
+
log('service-host: opening service context...');
|
|
398
396
|
await this._serviceContext.open(ctx);
|
|
397
|
+
log('service-host: service context opened');
|
|
398
|
+
|
|
399
|
+
log('service-host: opening identity service...');
|
|
399
400
|
await identityService.open();
|
|
401
|
+
log('service-host: identity service opened');
|
|
400
402
|
|
|
401
403
|
const devtoolsProxy = this._config?.get('runtime.client.devtoolsProxy');
|
|
402
404
|
if (devtoolsProxy) {
|