@dxos/client-services 0.8.4-main.69d29f4 → 0.8.4-main.6fa680abb7
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-KBXLEVD5.mjs → chunk-CZN7GX3A.mjs} +386 -121
- package/dist/lib/browser/chunk-CZN7GX3A.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-G62TR33T.mjs → chunk-F36CDLDY.mjs} +386 -121
- package/dist/lib/node-esm/chunk-F36CDLDY.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 +29 -8
- package/src/packlets/services/service-host.ts +0 -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-KBXLEVD5.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-G62TR33T.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()
|
|
@@ -240,6 +256,8 @@ export class ServiceContext extends Resource {
|
|
|
240
256
|
await this._initialize(ctx);
|
|
241
257
|
}
|
|
242
258
|
|
|
259
|
+
await this._feedSyncer?.open();
|
|
260
|
+
|
|
243
261
|
const loadedInvitations = await this.invitationsManager.loadPersistentInvitations();
|
|
244
262
|
log('loaded persistent invitations', { count: loadedInvitations.invitations?.length });
|
|
245
263
|
|
|
@@ -249,6 +267,9 @@ export class ServiceContext extends Resource {
|
|
|
249
267
|
|
|
250
268
|
protected override async _close(ctx: Context): Promise<void> {
|
|
251
269
|
log('closing...');
|
|
270
|
+
|
|
271
|
+
await this._feedSyncer?.close();
|
|
272
|
+
|
|
252
273
|
if (this._deviceSpaceSync && this.identityManager.identity) {
|
|
253
274
|
await this.identityManager.identity.space.spaceState.removeCredentialProcessor(this._deviceSpaceSync);
|
|
254
275
|
}
|
|
@@ -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;
|
|
@@ -37,6 +37,7 @@ export type CreateSessionProps = {
|
|
|
37
37
|
appPort: RpcPort;
|
|
38
38
|
systemPort: RpcPort;
|
|
39
39
|
shellPort?: RpcPort;
|
|
40
|
+
onClose?: () => Promise<void>;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
export type WorkerRuntimeOptions = {
|
|
@@ -50,7 +51,11 @@ export type WorkerRuntimeOptions = {
|
|
|
50
51
|
*/
|
|
51
52
|
automaticallyConnectWebrtc?: boolean;
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Optional SQLite layer for Effect. Defaults to LocalSqliteOpfsLayer.
|
|
56
|
+
* For testing in Node.js, use `sqliteLayerMemory` from `@dxos/sql-sqlite/platform`.
|
|
57
|
+
*/
|
|
58
|
+
sqliteLayer?: Layer.Layer<SqlClient.SqlClient | SqlExport.SqlExport, unknown>;
|
|
54
59
|
};
|
|
55
60
|
|
|
56
61
|
/**
|
|
@@ -87,16 +92,19 @@ export class WorkerRuntime {
|
|
|
87
92
|
releaseLock,
|
|
88
93
|
onStop,
|
|
89
94
|
automaticallyConnectWebrtc = true,
|
|
90
|
-
|
|
95
|
+
sqliteLayer,
|
|
91
96
|
}: WorkerRuntimeOptions) {
|
|
92
97
|
this._configProvider = configProvider;
|
|
93
98
|
this._acquireLock = acquireLock;
|
|
94
99
|
this._releaseLock = releaseLock;
|
|
95
100
|
this._onStop = onStop;
|
|
96
101
|
this._channel = channel;
|
|
102
|
+
if (sqliteLayer) {
|
|
103
|
+
log.warn('Using testing SQLite layer');
|
|
104
|
+
}
|
|
97
105
|
this._runtime = ManagedRuntime.make(
|
|
98
106
|
SqlTransaction.layer
|
|
99
|
-
.pipe(Layer.provideMerge(LocalSqliteOpfsLayer), Layer.provideMerge(Reactivity.layer))
|
|
107
|
+
.pipe(Layer.provideMerge(sqliteLayer ?? LocalSqliteOpfsLayer), Layer.provideMerge(Reactivity.layer))
|
|
100
108
|
.pipe(Layer.orDie),
|
|
101
109
|
);
|
|
102
110
|
this._clientServices = new ClientServicesHost({
|
|
@@ -105,7 +113,6 @@ export class WorkerRuntime {
|
|
|
105
113
|
},
|
|
106
114
|
runtime: this._runtime.runtimeEffect,
|
|
107
115
|
runtimeProps: {
|
|
108
|
-
enableSqlite,
|
|
109
116
|
// Auto-activate spaces that were previously active after leader changeover.
|
|
110
117
|
autoActivateSpaces: true,
|
|
111
118
|
},
|
|
@@ -178,7 +185,7 @@ export class WorkerRuntime {
|
|
|
178
185
|
/**
|
|
179
186
|
* Create a new session.
|
|
180
187
|
*/
|
|
181
|
-
async createSession({ appPort, systemPort, shellPort }: CreateSessionProps): Promise<WorkerSession> {
|
|
188
|
+
async createSession({ appPort, systemPort, shellPort, onClose }: CreateSessionProps): Promise<WorkerSession> {
|
|
182
189
|
const session = new WorkerSession({
|
|
183
190
|
serviceHost: this._clientServices,
|
|
184
191
|
appPort,
|
|
@@ -198,6 +205,7 @@ export class WorkerRuntime {
|
|
|
198
205
|
this._reconnectWebrtc();
|
|
199
206
|
}
|
|
200
207
|
}
|
|
208
|
+
await onClose?.();
|
|
201
209
|
});
|
|
202
210
|
|
|
203
211
|
await session.open();
|
|
@@ -119,7 +119,7 @@ export class WorkerSession {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
async open(): Promise<void> {
|
|
122
|
-
log
|
|
122
|
+
log('opening...');
|
|
123
123
|
await Promise.all([this._clientRpc.open(), this._iframeRpc.open(), this._maybeOpenShell()]);
|
|
124
124
|
|
|
125
125
|
// Wait until the worker's RPC service has started.
|
|
@@ -130,11 +130,11 @@ export class WorkerSession {
|
|
|
130
130
|
void this._afterLockReleases(this.lockKey, () => this.close());
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
log
|
|
133
|
+
log('opened');
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
async close(): Promise<void> {
|
|
137
|
-
log.
|
|
137
|
+
log.debug('closing...');
|
|
138
138
|
try {
|
|
139
139
|
await this.onClose.callIfSet();
|
|
140
140
|
} catch (err: any) {
|
|
@@ -142,7 +142,7 @@ export class WorkerSession {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
await Promise.all([this._clientRpc.close(), this._iframeRpc.close()]);
|
|
145
|
-
log.
|
|
145
|
+
log.debug('closed');
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
private async _maybeOpenShell(): Promise<void> {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const DXOS_VERSION = "0.8.4-main.
|
|
1
|
+
export const DXOS_VERSION = "0.8.4-main.6fa680abb7";
|