@vrtmrz/livesync-commonlib 0.2.2 → 0.3.0
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/README.md +86 -1
- package/dist/API/DirectFileManipulatorV2.d.ts +7 -1
- package/dist/API/DirectFileManipulatorV2.js +46 -67
- package/dist/API/DirectFileWatchOwner.d.ts +24 -0
- package/dist/API/DirectFileWatchOwner.js +256 -0
- package/dist/API/processSetting.js +3 -3
- package/dist/ContentSplitter/ContentSplitterRabinKarp.js +2 -17
- package/dist/ContentSplitter/ContentSplitterV1.js +9 -19
- package/dist/ContentSplitter/ContentSplitterV2.js +9 -19
- package/dist/common/models/setting.type.d.ts +1 -1
- package/dist/common/models/shared.definition.configNames.js +1 -2
- package/dist/common/models/tweak.definition.d.ts +1 -1
- package/dist/common/pathNormalization.d.ts +2 -0
- package/dist/common/pathNormalization.js +7 -2
- package/dist/common/utils.d.ts +33 -3
- package/dist/common/utils.js +12 -44
- package/dist/common/utils.object.js +2 -1
- package/dist/concurrency/keyedPendingWork.d.ts +13 -0
- package/dist/concurrency/keyedPendingWork.js +27 -0
- package/dist/conflicts/conflictCheckQueue.d.ts +45 -0
- package/dist/conflicts/conflictCheckQueue.js +108 -0
- package/dist/conflicts.d.ts +3 -0
- package/dist/conflicts.js +4 -0
- package/dist/diagnostics/integrity.js +11 -12
- package/dist/interfaces/ServiceModule.d.ts +42 -24
- package/dist/interfaces/ServiceModule.js +4 -3
- package/dist/managers/ChunkFetcher.d.ts +3 -0
- package/dist/managers/ChunkFetcher.js +20 -13
- package/dist/managers/LiveSyncManagers.d.ts +2 -0
- package/dist/managers/LiveSyncManagers.js +1 -0
- package/dist/managers/StorageEventManager.d.ts +3 -3
- package/dist/managers/StorageEventManager.js +15 -6
- package/dist/managers/chunkFetchSource.d.ts +14 -0
- package/dist/managers/chunkFetchSource.js +12 -0
- package/dist/pouchdb/LiveSyncDBFunctions.js +5 -0
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +7 -1
- package/dist/pouchdb/LiveSyncLocalDB.js +14 -10
- package/dist/pouchdb/chunks.d.ts +16 -0
- package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
- package/dist/pouchdb/pathIdentityCompatibility.js +19 -1
- package/dist/pouchdb/pouchdb-browser.js +2 -84
- package/dist/pouchdb/pouchdb-http.js +2 -84
- package/dist/pouchdb/purgeMulti.d.ts +4 -0
- package/dist/pouchdb/purgeMulti.js +89 -0
- package/dist/replication/RemoteResource.d.ts +3 -0
- package/dist/replication/index.d.ts +2 -0
- package/dist/replication/index.js +2 -0
- package/dist/replication/journal/JournalSyncCore.d.ts +1 -0
- package/dist/replication/journal/JournalSyncCore.js +41 -20
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +11 -3
- package/dist/replication/journal/LiveSyncJournalReplicator.js +123 -45
- package/dist/replication/trystero/P2PRoomSessionOwner.js +14 -13
- package/dist/replication/trystero/TrysteroReplicator.d.ts +7 -5
- package/dist/replication/trystero/TrysteroReplicator.js +101 -74
- package/dist/replication/trystero/TrysteroReplicator.pathIdentity.d.ts +11 -0
- package/dist/replication/trystero/TrysteroReplicator.pathIdentity.js +72 -0
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +3 -2
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +2 -0
- package/dist/replication/trystero/peerPatterns.d.ts +1 -0
- package/dist/replication/trystero/peerPatterns.js +6 -0
- package/dist/replication/trystero/useP2PReplicatorFeature.js +6 -2
- package/dist/rpc/RpcRoom.d.ts +5 -1
- package/dist/rpc/RpcRoom.js +80 -105
- package/dist/serviceFeatures/offlineScanner.d.ts +4 -4
- package/dist/serviceFeatures/offlineScanner.js +14 -15
- package/dist/serviceFeatures/prepareDatabaseForUse.d.ts +2 -2
- package/dist/serviceFeatures/remoteConfig.d.ts +10 -5
- package/dist/serviceModules/Rebuilder.d.ts +1 -0
- package/dist/serviceModules/Rebuilder.js +33 -5
- package/dist/serviceModules/ServiceFileHandlerBase.js +2 -7
- package/dist/services/implements/injectable/InjectableConflictService.d.ts +5 -5
- package/dist/services/lib/HandlerUtils.d.ts +8 -6
- package/dist/services/lib/HandlerUtils.js +14 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/conflict-resolution.md +30 -0
- package/docs/development.md +50 -0
- package/docs/proven-in-use.md +80 -0
- package/docs/releasing.md +4 -0
- package/docs/service-feature-composition.md +63 -10
- package/docs/unicode-path-identity.md +64 -3
- package/package.json +11 -3
package/dist/rpc/RpcRoom.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { compatGlobal } from "../common/coreEnvFunctions.js";
|
|
2
|
+
import {
|
|
3
|
+
createDeferredAction
|
|
4
|
+
} from "octagonal-wheels/concurrency/deferredAction";
|
|
2
5
|
import { IncomingChunkBuffer, estimateBytes, splitIntoChunks } from "./chunking.js";
|
|
3
6
|
import { asRpcErrorShape, RpcError } from "./errors.js";
|
|
4
7
|
import { RpcSession } from "./RpcSession.js";
|
|
@@ -12,6 +15,10 @@ function newId(prefix) {
|
|
|
12
15
|
function validNamespacedMethod(method) {
|
|
13
16
|
return method.includes(".") || method.includes("/");
|
|
14
17
|
}
|
|
18
|
+
const scheduleRpcTimeout = (callback, delayMs) => {
|
|
19
|
+
const handle = compatGlobal.setTimeout(callback, delayMs);
|
|
20
|
+
return () => compatGlobal.clearTimeout(handle);
|
|
21
|
+
};
|
|
15
22
|
class RpcRoom {
|
|
16
23
|
options;
|
|
17
24
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -21,7 +28,7 @@ class RpcRoom {
|
|
|
21
28
|
sessions = /* @__PURE__ */ new Map();
|
|
22
29
|
outgoingChunkMap = /* @__PURE__ */ new Map();
|
|
23
30
|
incomingChunkMap = /* @__PURE__ */ new Map();
|
|
24
|
-
|
|
31
|
+
incomingChunkWaits = /* @__PURE__ */ new Map();
|
|
25
32
|
peerVersion = /* @__PURE__ */ new Map();
|
|
26
33
|
disposers = [];
|
|
27
34
|
closed = false;
|
|
@@ -83,8 +90,8 @@ class RpcRoom {
|
|
|
83
90
|
this.sessions.clear();
|
|
84
91
|
this.outgoingChunkMap.clear();
|
|
85
92
|
this.incomingChunkMap.clear();
|
|
86
|
-
this.
|
|
87
|
-
this.
|
|
93
|
+
this.incomingChunkWaits.forEach((wait) => wait.dispose());
|
|
94
|
+
this.incomingChunkWaits.clear();
|
|
88
95
|
}
|
|
89
96
|
session(peerId) {
|
|
90
97
|
const existing = this.sessions.get(peerId);
|
|
@@ -125,30 +132,32 @@ class RpcRoom {
|
|
|
125
132
|
const requestId = newId("req");
|
|
126
133
|
let pending;
|
|
127
134
|
const p = new Promise((resolve, reject) => {
|
|
135
|
+
const signal = options.signal;
|
|
136
|
+
const onAbort = () => {
|
|
137
|
+
const current = this.pending.get(requestId);
|
|
138
|
+
if (!current) return;
|
|
139
|
+
this.rejectPending(requestId, new RpcError("CANCELLED", `RPC cancelled: ${method}`), true);
|
|
140
|
+
};
|
|
141
|
+
const deadline = timeoutMs > 0 ? createDeferredAction(() => {
|
|
142
|
+
this.rejectPending(requestId, new RpcError("TIMEOUT", `RPC timed out: ${method}`), true);
|
|
143
|
+
}, scheduleRpcTimeout) : void 0;
|
|
128
144
|
pending = {
|
|
129
145
|
peerId,
|
|
130
146
|
method,
|
|
131
147
|
resolve,
|
|
132
148
|
reject,
|
|
133
|
-
|
|
149
|
+
releaseWait: () => {
|
|
150
|
+
deadline?.dispose();
|
|
151
|
+
signal?.removeEventListener("abort", onAbort);
|
|
152
|
+
},
|
|
134
153
|
requestSent: false,
|
|
135
154
|
cancelRequested: false
|
|
136
155
|
};
|
|
137
|
-
|
|
138
|
-
pending.timeoutHandle = compatGlobal.setTimeout(() => {
|
|
139
|
-
this.rejectPending(requestId, new RpcError("TIMEOUT", `RPC timed out: ${method}`), true);
|
|
140
|
-
}, timeoutMs);
|
|
141
|
-
}
|
|
156
|
+
deadline?.schedule(timeoutMs);
|
|
142
157
|
this.pending.set(requestId, pending);
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!current) return;
|
|
147
|
-
this.rejectPending(requestId, new RpcError("CANCELLED", `RPC cancelled: ${method}`), true);
|
|
148
|
-
};
|
|
149
|
-
pending.abortListener = onAbort;
|
|
150
|
-
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
151
|
-
if (options.signal.aborted) onAbort();
|
|
158
|
+
if (signal) {
|
|
159
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
160
|
+
if (signal.aborted) onAbort();
|
|
152
161
|
}
|
|
153
162
|
});
|
|
154
163
|
const sendTask = this.sendEnvelope(peerId, {
|
|
@@ -191,12 +200,7 @@ class RpcRoom {
|
|
|
191
200
|
const pending = this.pending.get(requestId);
|
|
192
201
|
if (!pending) return void 0;
|
|
193
202
|
this.pending.delete(requestId);
|
|
194
|
-
|
|
195
|
-
compatGlobal.clearTimeout(pending.timeoutHandle);
|
|
196
|
-
}
|
|
197
|
-
if (pending.signal && pending.abortListener) {
|
|
198
|
-
pending.signal.removeEventListener("abort", pending.abortListener);
|
|
199
|
-
}
|
|
203
|
+
pending.releaseWait();
|
|
200
204
|
return pending;
|
|
201
205
|
}
|
|
202
206
|
rejectPending(requestId, error, notifyRemote = false) {
|
|
@@ -229,28 +233,25 @@ class RpcRoom {
|
|
|
229
233
|
const chunks = splitIntoChunks(serialized, this.options.maxWirePayloadBytes);
|
|
230
234
|
this.outgoingChunkMap.set(streamId, { peerId, chunks });
|
|
231
235
|
for (let i = 0; i < chunks.length; i++) {
|
|
232
|
-
await this.
|
|
233
|
-
{
|
|
234
|
-
wire: "chunk",
|
|
235
|
-
streamId,
|
|
236
|
-
index: i,
|
|
237
|
-
total: chunks.length,
|
|
238
|
-
payload: chunks[i]
|
|
239
|
-
},
|
|
240
|
-
peerId
|
|
241
|
-
);
|
|
236
|
+
await this.sendChunk(peerId, streamId, chunks.length, i, chunks[i]);
|
|
242
237
|
}
|
|
243
238
|
}
|
|
239
|
+
sendChunk(peerId, streamId, total, index, payload) {
|
|
240
|
+
return this.options.transport.send({ wire: "chunk", streamId, index, total, payload }, peerId);
|
|
241
|
+
}
|
|
242
|
+
sendErrorResponse(peerId, requestId, error) {
|
|
243
|
+
return this.sendEnvelope(peerId, { kind: "response", requestId, ok: false, error });
|
|
244
|
+
}
|
|
244
245
|
scheduleMissingAck(streamId, peerId) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const handle = compatGlobal.setTimeout(() => {
|
|
246
|
+
this.incomingChunkWaits.get(streamId)?.dispose();
|
|
247
|
+
const wait = createDeferredAction(() => {
|
|
248
248
|
const state = this.incomingChunkMap.get(streamId);
|
|
249
249
|
if (!state || state.isComplete()) return;
|
|
250
250
|
const missing = state.missingIndices();
|
|
251
251
|
void this.options.transport.send({ wire: "chunk-ack", streamId, missing }, peerId);
|
|
252
|
-
},
|
|
253
|
-
this.
|
|
252
|
+
}, scheduleRpcTimeout);
|
|
253
|
+
wait.schedule(this.options.chunkMissingRetryMs);
|
|
254
|
+
this.incomingChunkWaits.set(streamId, wait);
|
|
254
255
|
}
|
|
255
256
|
async onWireMessage(message, peerId) {
|
|
256
257
|
if (message.wire === "raw") {
|
|
@@ -266,9 +267,8 @@ class RpcRoom {
|
|
|
266
267
|
state2.add(message.index, message.payload);
|
|
267
268
|
this.scheduleMissingAck(message.streamId, peerId);
|
|
268
269
|
if (state2.isComplete()) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
this.incomingChunkTimers.delete(message.streamId);
|
|
270
|
+
this.incomingChunkWaits.get(message.streamId)?.dispose();
|
|
271
|
+
this.incomingChunkWaits.delete(message.streamId);
|
|
272
272
|
this.incomingChunkMap.delete(message.streamId);
|
|
273
273
|
await this.options.transport.send(
|
|
274
274
|
{ wire: "chunk-ack", streamId: message.streamId, missing: [] },
|
|
@@ -287,16 +287,7 @@ class RpcRoom {
|
|
|
287
287
|
for (const index of message.missing) {
|
|
288
288
|
const payload = state.chunks[index];
|
|
289
289
|
if (payload === void 0) continue;
|
|
290
|
-
await this.
|
|
291
|
-
{
|
|
292
|
-
wire: "chunk",
|
|
293
|
-
streamId: message.streamId,
|
|
294
|
-
index,
|
|
295
|
-
total: state.chunks.length,
|
|
296
|
-
payload
|
|
297
|
-
},
|
|
298
|
-
state.peerId
|
|
299
|
-
);
|
|
290
|
+
await this.sendChunk(state.peerId, message.streamId, state.chunks.length, index, payload);
|
|
300
291
|
}
|
|
301
292
|
}
|
|
302
293
|
async onEnvelopePayload(payload, peerId) {
|
|
@@ -319,8 +310,8 @@ class RpcRoom {
|
|
|
319
310
|
return;
|
|
320
311
|
}
|
|
321
312
|
if (envelope.kind === "cancel") {
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
313
|
+
const ctx = this.inboundCalls.get(envelope.requestId);
|
|
314
|
+
if (ctx && ctx.peerId === peerId) this.abortInboundCall(envelope.requestId, ctx);
|
|
324
315
|
return;
|
|
325
316
|
}
|
|
326
317
|
if (envelope.kind === "response") {
|
|
@@ -335,6 +326,9 @@ class RpcRoom {
|
|
|
335
326
|
}
|
|
336
327
|
return;
|
|
337
328
|
}
|
|
329
|
+
return this.onRequest(envelope, peerId);
|
|
330
|
+
}
|
|
331
|
+
async onRequest(envelope, peerId) {
|
|
338
332
|
const controller = new AbortController();
|
|
339
333
|
const ctx = { controller, signal: controller.signal, peerId };
|
|
340
334
|
this.inboundCalls.set(envelope.requestId, ctx);
|
|
@@ -344,12 +338,7 @@ class RpcRoom {
|
|
|
344
338
|
} catch (ex) {
|
|
345
339
|
this.inboundCalls.delete(envelope.requestId);
|
|
346
340
|
if (!ctx.signal.aborted && !this.closed) {
|
|
347
|
-
await this.
|
|
348
|
-
kind: "response",
|
|
349
|
-
requestId: envelope.requestId,
|
|
350
|
-
ok: false,
|
|
351
|
-
error: asRpcErrorShape(ex)
|
|
352
|
-
});
|
|
341
|
+
await this.sendErrorResponse(peerId, envelope.requestId, asRpcErrorShape(ex));
|
|
353
342
|
}
|
|
354
343
|
return;
|
|
355
344
|
}
|
|
@@ -364,14 +353,9 @@ class RpcRoom {
|
|
|
364
353
|
const version = this.peerVersion.get(peerId);
|
|
365
354
|
if (version && version.major !== RPC_VERSION_MAJOR) {
|
|
366
355
|
if (!ctx.signal.aborted && !this.closed) {
|
|
367
|
-
await this.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
ok: false,
|
|
371
|
-
error: {
|
|
372
|
-
code: "REMOTE_ERROR",
|
|
373
|
-
message: `RPC major mismatch: local=${RPC_VERSION_MAJOR}, remote=${version.major}`
|
|
374
|
-
}
|
|
356
|
+
await this.sendErrorResponse(peerId, envelope.requestId, {
|
|
357
|
+
code: "REMOTE_ERROR",
|
|
358
|
+
message: `RPC major mismatch: local=${RPC_VERSION_MAJOR}, remote=${version.major}`
|
|
375
359
|
});
|
|
376
360
|
}
|
|
377
361
|
this.inboundCalls.delete(envelope.requestId);
|
|
@@ -386,48 +370,15 @@ class RpcRoom {
|
|
|
386
370
|
const method = this.methods.get(envelope.method);
|
|
387
371
|
if (!method) {
|
|
388
372
|
if (!ctx.signal.aborted && !this.closed) {
|
|
389
|
-
await this.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
ok: false,
|
|
393
|
-
error: {
|
|
394
|
-
code: "REMOTE_ERROR",
|
|
395
|
-
message: `Method not found: ${envelope.method}`
|
|
396
|
-
}
|
|
373
|
+
await this.sendErrorResponse(peerId, envelope.requestId, {
|
|
374
|
+
code: "REMOTE_ERROR",
|
|
375
|
+
message: `Method not found: ${envelope.method}`
|
|
397
376
|
});
|
|
398
377
|
}
|
|
399
378
|
this.inboundCalls.delete(envelope.requestId);
|
|
400
379
|
return;
|
|
401
380
|
}
|
|
402
|
-
const runner =
|
|
403
|
-
try {
|
|
404
|
-
if (ctx.signal.aborted || this.closed) return;
|
|
405
|
-
const data = method.cancellationAware ? await method.handler(
|
|
406
|
-
ctx,
|
|
407
|
-
peerId,
|
|
408
|
-
...envelope.args
|
|
409
|
-
) : await method.handler(peerId, ...envelope.args);
|
|
410
|
-
if (ctx.signal.aborted || this.closed) return;
|
|
411
|
-
await this.sendEnvelope(peerId, {
|
|
412
|
-
kind: "response",
|
|
413
|
-
requestId: envelope.requestId,
|
|
414
|
-
ok: true,
|
|
415
|
-
data
|
|
416
|
-
});
|
|
417
|
-
} catch (ex) {
|
|
418
|
-
if (ctx.signal.aborted || this.closed) return;
|
|
419
|
-
await this.sendEnvelope(peerId, {
|
|
420
|
-
kind: "response",
|
|
421
|
-
requestId: envelope.requestId,
|
|
422
|
-
ok: false,
|
|
423
|
-
error: asRpcErrorShape(ex)
|
|
424
|
-
});
|
|
425
|
-
} finally {
|
|
426
|
-
if (this.inboundCalls.get(envelope.requestId) === ctx) {
|
|
427
|
-
this.inboundCalls.delete(envelope.requestId);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
};
|
|
381
|
+
const runner = () => this.runMethod(method, envelope, peerId, ctx);
|
|
431
382
|
if (method.serial) {
|
|
432
383
|
method.queue = method.queue.then(runner, runner);
|
|
433
384
|
await method.queue;
|
|
@@ -435,6 +386,30 @@ class RpcRoom {
|
|
|
435
386
|
await runner();
|
|
436
387
|
}
|
|
437
388
|
}
|
|
389
|
+
async runMethod(method, envelope, peerId, ctx) {
|
|
390
|
+
try {
|
|
391
|
+
if (ctx.signal.aborted || this.closed) return;
|
|
392
|
+
const data = method.cancellationAware ? await method.handler(
|
|
393
|
+
ctx,
|
|
394
|
+
peerId,
|
|
395
|
+
...envelope.args
|
|
396
|
+
) : await method.handler(peerId, ...envelope.args);
|
|
397
|
+
if (ctx.signal.aborted || this.closed) return;
|
|
398
|
+
await this.sendEnvelope(peerId, {
|
|
399
|
+
kind: "response",
|
|
400
|
+
requestId: envelope.requestId,
|
|
401
|
+
ok: true,
|
|
402
|
+
data
|
|
403
|
+
});
|
|
404
|
+
} catch (ex) {
|
|
405
|
+
if (ctx.signal.aborted || this.closed) return;
|
|
406
|
+
await this.sendErrorResponse(peerId, envelope.requestId, asRpcErrorShape(ex));
|
|
407
|
+
} finally {
|
|
408
|
+
if (this.inboundCalls.get(envelope.requestId) === ctx) {
|
|
409
|
+
this.inboundCalls.delete(envelope.requestId);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
438
413
|
}
|
|
439
414
|
export {
|
|
440
415
|
RpcRoom
|
|
@@ -245,7 +245,7 @@ export declare function resolveFilePairAction(state: FilePairState, options: Ful
|
|
|
245
245
|
* @param options Full scan options
|
|
246
246
|
* @returns The scan result for all file pairs
|
|
247
247
|
*/
|
|
248
|
-
export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "
|
|
248
|
+
export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options: FullScanOptions): Promise<VaultScanResult>;
|
|
249
249
|
export declare function normaliseFullScanOptions(showingNoticeOrOptions: Partial<FullScanOptions> | boolean | undefined, ignoreSuspending?: boolean): FullScanOptions;
|
|
250
250
|
/**
|
|
251
251
|
* Remove one path from the Offline Scanner's durable last-seen state.
|
|
@@ -264,11 +264,11 @@ export declare function clearOfflineScannerLastSeen(host: NecessaryServices<"set
|
|
|
264
264
|
* @param ignoreSuspending Legacy suspension flag used with the notice flag
|
|
265
265
|
* @returns The scan outcome, including accepted individual file failures
|
|
266
266
|
*/
|
|
267
|
-
export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "
|
|
268
|
-
export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "
|
|
267
|
+
export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options?: Partial<FullScanOptions>): Promise<VaultScanResult>;
|
|
268
|
+
export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, ignoreSuspending?: boolean): Promise<VaultScanResult>;
|
|
269
269
|
/**
|
|
270
270
|
* Associate the initialiser file feature with the app lifecycle events.
|
|
271
271
|
* This function binds initialization handlers to the appropriate lifecycle events.
|
|
272
272
|
* @param host Services container with required dependencies
|
|
273
273
|
*/
|
|
274
|
-
export declare function useOfflineScanner(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "
|
|
274
|
+
export declare function useOfflineScanner(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "keyValueDB", "storageAccess" | "fileHandler">): void;
|
|
@@ -15,7 +15,7 @@ import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "../common/models/shared.const.
|
|
|
15
15
|
import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
|
|
16
16
|
import { compatGlobal } from "../common/coreEnvFunctions.js";
|
|
17
17
|
import { ICHeader, ICXHeader, PSCHeader } from "../common/models/fileaccess.const.js";
|
|
18
|
-
import { serialized } from "octagonal-wheels/concurrency/lock";
|
|
18
|
+
import { serialized, withLocks } from "octagonal-wheels/concurrency/lock";
|
|
19
19
|
import { VaultScanResults } from "../services/base/VaultScanResult.js";
|
|
20
20
|
const FilePairProcessResults = {
|
|
21
21
|
COMPLETED: "completed",
|
|
@@ -218,17 +218,10 @@ function createReaddressedMetadata(source, expectedDocumentId) {
|
|
|
218
218
|
}
|
|
219
219
|
async function serializedByMetadataDocumentIds(documentIds, callback) {
|
|
220
220
|
const lockKeys = [...new Set(documentIds)].sort().map((documentId) => `processFileEvent-${documentId}`);
|
|
221
|
-
|
|
222
|
-
const key = lockKeys[index];
|
|
223
|
-
if (key === void 0) return await callback();
|
|
224
|
-
return await serialized(key, () => acquire(index + 1));
|
|
225
|
-
};
|
|
226
|
-
return await acquire(0);
|
|
221
|
+
return await withLocks(lockKeys, callback);
|
|
227
222
|
}
|
|
228
223
|
function findRepairEntry(entries, actualDocumentId) {
|
|
229
|
-
return entries.find(
|
|
230
|
-
({ inspection }) => inspection.diagnostic.actualDocumentId === actualDocumentId
|
|
231
|
-
);
|
|
224
|
+
return entries.find(({ inspection }) => inspection.diagnostic.actualDocumentId === actualDocumentId);
|
|
232
225
|
}
|
|
233
226
|
async function repairMetadataDocumentIdentity(host, request) {
|
|
234
227
|
const baseResult = {
|
|
@@ -443,6 +436,9 @@ function convertCase(settings, path) {
|
|
|
443
436
|
}
|
|
444
437
|
return normalisedPath.toLowerCase();
|
|
445
438
|
}
|
|
439
|
+
function createComparisonPathLookup(settings, paths) {
|
|
440
|
+
return Object.fromEntries(paths.map((path) => [convertCase(settings, path), path]));
|
|
441
|
+
}
|
|
446
442
|
async function collectFilesOnStorage(host, settings, log) {
|
|
447
443
|
log("Collecting local files on the storage", LOG_LEVEL_VERBOSE);
|
|
448
444
|
const filesStorageSrc = await host.serviceModules.storageAccess.getFiles();
|
|
@@ -455,8 +451,7 @@ async function collectFilesOnStorage(host, settings, log) {
|
|
|
455
451
|
}
|
|
456
452
|
const storageFileNameMap = Object.fromEntries(_filesStorage.map((e) => [e.path, e]));
|
|
457
453
|
const storageFileNames = Object.keys(storageFileNameMap);
|
|
458
|
-
const
|
|
459
|
-
const storageFileNameCI2CS = Object.fromEntries(storageFileNameCapsPair.map((e) => [e[1], e[0]]));
|
|
454
|
+
const storageFileNameCI2CS = createComparisonPathLookup(settings, storageFileNames);
|
|
460
455
|
return { storageFileNameMap, storageFileNames, storageFileNameCI2CS };
|
|
461
456
|
}
|
|
462
457
|
async function collectDatabaseFiles(host, settings, log, showingNotice) {
|
|
@@ -500,8 +495,7 @@ async function collectDatabaseFiles(host, settings, log, showingNotice) {
|
|
|
500
495
|
}
|
|
501
496
|
const databaseFileNameMap = Object.fromEntries(_DBEntries.map((e) => [getPathFromEntry(host, e), e]));
|
|
502
497
|
const databaseFileNames = Object.keys(databaseFileNameMap);
|
|
503
|
-
const
|
|
504
|
-
const databaseFileNameCI2CS = Object.fromEntries(databaseFileNameCapsPair.map((e) => [e[1], e[0]]));
|
|
498
|
+
const databaseFileNameCI2CS = createComparisonPathLookup(settings, databaseFileNames);
|
|
505
499
|
const quarantinedFileNamesLC = new Set(
|
|
506
500
|
[...unresolvedFileNamesLC].filter((path) => !resolvableFileNamesLC.has(path))
|
|
507
501
|
);
|
|
@@ -750,7 +744,12 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
|
|
|
750
744
|
const showingNotice = options.showingNotice ?? false;
|
|
751
745
|
await loadFileStatus(host);
|
|
752
746
|
const { storageFileNameMap, storageFileNameCI2CS } = await collectFilesOnStorage(host, settings, log);
|
|
753
|
-
const { databaseFileNameMap, databaseFileNameCI2CS, quarantinedFileNamesLC } = await collectDatabaseFiles(
|
|
747
|
+
const { databaseFileNameMap, databaseFileNameCI2CS, quarantinedFileNamesLC } = await collectDatabaseFiles(
|
|
748
|
+
host,
|
|
749
|
+
settings,
|
|
750
|
+
log,
|
|
751
|
+
showingNotice
|
|
752
|
+
);
|
|
754
753
|
const pairs = [];
|
|
755
754
|
for (const fileNameLC of unique([
|
|
756
755
|
...Object.keys(storageFileNameCI2CS),
|
|
@@ -14,10 +14,10 @@ import { type VaultScanResult, type VaultScanOptions } from "../services/base/Va
|
|
|
14
14
|
* @param scanOptions Optional content-check policy; omitted options keep strict checks
|
|
15
15
|
* @returns The initialisation outcome, including accepted individual file failures
|
|
16
16
|
*/
|
|
17
|
-
export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "
|
|
17
|
+
export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "vault" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean, scanOptions?: VaultScanOptions): Promise<VaultScanResult>;
|
|
18
18
|
/**
|
|
19
19
|
* Associate the initialiser file feature with the app lifecycle events.
|
|
20
20
|
* This function binds initialization handlers to the appropriate lifecycle events.
|
|
21
21
|
* @param host Services container with required dependencies
|
|
22
22
|
*/
|
|
23
|
-
export declare function usePrepareDatabaseForUse(host: NecessaryServices<"API" | "appLifecycle" | "
|
|
23
|
+
export declare function usePrepareDatabaseForUse(host: NecessaryServices<"API" | "appLifecycle" | "vault" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>): void;
|
|
@@ -3,6 +3,10 @@ import { type RemoteConfigurationResult } from "../common/ConnectionString.js";
|
|
|
3
3
|
import type { ObsidianLiveSyncSettings, RemoteConfiguration } from "../common/models/setting.type.js";
|
|
4
4
|
import type { NecessaryServices } from "../interfaces/ServiceModule.js";
|
|
5
5
|
export type RemoteConfigHost = NecessaryServices<"setting" | "UI" | "replication" | "control" | "appLifecycle" | "API", never>;
|
|
6
|
+
type RemoteConfigMigrationHost = NecessaryServices<"setting" | "API", never>;
|
|
7
|
+
type RemoteConfigMigrationFeatureHost = NecessaryServices<"setting" | "API" | "appLifecycle", never>;
|
|
8
|
+
type RemoteConfigSwitchHost = NecessaryServices<"setting" | "UI" | "control" | "API", never>;
|
|
9
|
+
type RemoteConfigReplicationHost = NecessaryServices<"setting" | "UI" | "replication" | "control" | "API", never>;
|
|
6
10
|
export declare function migrateLegacyRemoteConfigurationsInPlace(settings: ObsidianLiveSyncSettings, log?: (message: string, level?: LOG_LEVEL) => void): boolean;
|
|
7
11
|
/**
|
|
8
12
|
* Generate a unique ID for a new remote configuration.
|
|
@@ -54,7 +58,7 @@ export declare function migrateP2PActiveRemoteConfigurationIdInPlace(settings: O
|
|
|
54
58
|
/**
|
|
55
59
|
* Migrates existing flat settings to the new multiple remote configurations list.
|
|
56
60
|
*/
|
|
57
|
-
export declare function migrateToMultipleRemoteConfigurations(host:
|
|
61
|
+
export declare function migrateToMultipleRemoteConfigurations(host: RemoteConfigMigrationHost): Promise<boolean>;
|
|
58
62
|
/**
|
|
59
63
|
* Logic to switch the active configuration.
|
|
60
64
|
*/
|
|
@@ -67,16 +71,17 @@ export declare function activateP2PRemoteConfiguration(settings: ObsidianLiveSyn
|
|
|
67
71
|
/**
|
|
68
72
|
* Command: Switch active connection
|
|
69
73
|
*/
|
|
70
|
-
export declare function commandSwitchActiveRemote(host:
|
|
74
|
+
export declare function commandSwitchActiveRemote(host: RemoteConfigSwitchHost): Promise<void>;
|
|
71
75
|
/**
|
|
72
76
|
* Command: Sync with a saved connection
|
|
73
77
|
*/
|
|
74
|
-
export declare function commandReplicateWithSpecificRemote(host:
|
|
78
|
+
export declare function commandReplicateWithSpecificRemote(host: RemoteConfigReplicationHost): Promise<void>;
|
|
75
79
|
/**
|
|
76
80
|
* Migration feature to be used during initialisation.
|
|
77
81
|
*/
|
|
78
|
-
export declare function useRemoteConfigurationMigration(host:
|
|
82
|
+
export declare function useRemoteConfigurationMigration(host: RemoteConfigMigrationFeatureHost): void;
|
|
79
83
|
/**
|
|
80
84
|
* Hook to set up remote configuration features (Commands).
|
|
81
85
|
*/
|
|
82
|
-
export declare function useRemoteConfiguration(host:
|
|
86
|
+
export declare function useRemoteConfiguration(host: RemoteConfigReplicationHost): boolean;
|
|
87
|
+
export {};
|
|
@@ -66,6 +66,7 @@ export declare class ServiceRebuilder extends ServiceModuleBase<ServiceRebuilder
|
|
|
66
66
|
private scheduleInitialisation;
|
|
67
67
|
scheduleRebuild(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
|
|
68
68
|
scheduleFetch(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
|
|
69
|
+
private markRemoteLockedForRebuild;
|
|
69
70
|
private _tryResetRemoteDatabase;
|
|
70
71
|
private _onResetLocalDatabase;
|
|
71
72
|
suspendAllSync(): Promise<void>;
|
|
@@ -26,6 +26,12 @@ const FAST_FETCH_RETRY_DELAYS = [2e3, 5e3, 1e4, 2e4];
|
|
|
26
26
|
function canResetCentralRemote(replicator) {
|
|
27
27
|
return "tryResetRemoteDatabase" in replicator && typeof replicator.tryResetRemoteDatabase === "function";
|
|
28
28
|
}
|
|
29
|
+
function canCheckJournalPathIdentity(replicator) {
|
|
30
|
+
return "assertRemotePathIdentity" in replicator && typeof replicator.assertRemotePathIdentity === "function";
|
|
31
|
+
}
|
|
32
|
+
function canLockJournalForRebuild(replicator) {
|
|
33
|
+
return canResetCentralRemote(replicator) && "markRemoteLockedForRebuild" in replicator && typeof replicator.markRemoteLockedForRebuild === "function";
|
|
34
|
+
}
|
|
29
35
|
class ServiceRebuilder extends ServiceModuleBase {
|
|
30
36
|
events;
|
|
31
37
|
appLifecycle;
|
|
@@ -100,7 +106,7 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
100
106
|
notifyThresholdOfRemoteStorageSize: DEFAULT_SETTINGS.notifyThresholdOfRemoteStorageSize
|
|
101
107
|
});
|
|
102
108
|
await this.control.applySettings();
|
|
103
|
-
await this.
|
|
109
|
+
await this.markRemoteLockedForRebuild();
|
|
104
110
|
await this._tryResetRemoteDatabase();
|
|
105
111
|
await this.replication.markLocked();
|
|
106
112
|
await delay(500);
|
|
@@ -145,7 +151,7 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
145
151
|
}
|
|
146
152
|
return;
|
|
147
153
|
}
|
|
148
|
-
await this.
|
|
154
|
+
await this.markRemoteLockedForRebuild();
|
|
149
155
|
await this._tryResetRemoteDatabase();
|
|
150
156
|
await this.replication.markLocked();
|
|
151
157
|
await delay(500);
|
|
@@ -170,9 +176,6 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
170
176
|
await assertDatabasePathIdentity(this.database.localDatabase.localDatabase, settings);
|
|
171
177
|
}
|
|
172
178
|
if (getPathNormalizationVersion(settings) === 0) return;
|
|
173
|
-
if (settings.remoteType !== REMOTE_COUCHDB) {
|
|
174
|
-
throw new Error("NFC path identity is supported only with CouchDB. No database has been reset.");
|
|
175
|
-
}
|
|
176
179
|
const paths = await this.storageAccess.getFileNames();
|
|
177
180
|
const selected = [];
|
|
178
181
|
const databaseReady = this.database.isDatabaseReady();
|
|
@@ -191,6 +194,19 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
191
194
|
}
|
|
192
195
|
async checkRemotePathIdentityBeforeFetch() {
|
|
193
196
|
const settings = this.setting.currentSettings();
|
|
197
|
+
if (settings.remoteType === REMOTE_MINIO) {
|
|
198
|
+
const trial = await this.replicator.getNewReplicator(settings);
|
|
199
|
+
if (!trial) throw new Error("Could not create a Journal replicator for the Fetch identity check.");
|
|
200
|
+
try {
|
|
201
|
+
if (!canCheckJournalPathIdentity(trial)) {
|
|
202
|
+
throw new Error("The Journal replicator does not support the Fetch identity check.");
|
|
203
|
+
}
|
|
204
|
+
await trial.assertRemotePathIdentity(settings);
|
|
205
|
+
} finally {
|
|
206
|
+
await trial.closeReplication();
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
194
210
|
if (settings.remoteType !== REMOTE_COUCHDB) return;
|
|
195
211
|
const remote = settings.couchDB_URI.replace(/\/+$/, "") + (settings.couchDB_DBNAME === "" ? "" : "/" + settings.couchDB_DBNAME);
|
|
196
212
|
const connection = await this.remote.connect(
|
|
@@ -262,6 +278,18 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
262
278
|
scheduleFetch(prepareBeforeRestart) {
|
|
263
279
|
return this.scheduleInitialisation(FlagFilesHumanReadable.FETCH_ALL, prepareBeforeRestart);
|
|
264
280
|
}
|
|
281
|
+
async markRemoteLockedForRebuild() {
|
|
282
|
+
const settings = this.setting.currentSettings();
|
|
283
|
+
if (settings.remoteType !== REMOTE_MINIO) {
|
|
284
|
+
await this.replication.markLocked();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const replicator = this.replicator.getActiveReplicator();
|
|
288
|
+
if (!replicator || !canLockJournalForRebuild(replicator)) {
|
|
289
|
+
throw new Error("The active Journal replicator does not support safe reconstruction.");
|
|
290
|
+
}
|
|
291
|
+
await replicator.markRemoteLockedForRebuild(settings);
|
|
292
|
+
}
|
|
265
293
|
async _tryResetRemoteDatabase() {
|
|
266
294
|
const currentReplicator = this.replicator.getActiveReplicator();
|
|
267
295
|
const settings = this.setting.currentSettings();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
|
2
|
-
import {
|
|
2
|
+
import { withLocks } from "octagonal-wheels/concurrency/lock";
|
|
3
3
|
import {
|
|
4
4
|
createBlob,
|
|
5
5
|
getDocDataAsArray,
|
|
@@ -15,11 +15,6 @@ import { getStoragePathFromUXFileInfo } from "../common/typeUtils.js";
|
|
|
15
15
|
import { EVEN } from "../common/models/shared.const.symbols.js";
|
|
16
16
|
import { tryGetFilePath } from "../common/utils.doc.js";
|
|
17
17
|
import { getPathNormalizationVersion, normalisePathForIdentity } from "../common/pathNormalization.js";
|
|
18
|
-
async function serializedByKeys(keys, callback) {
|
|
19
|
-
const [key, ...remainingKeys] = keys;
|
|
20
|
-
if (key === void 0) return await callback();
|
|
21
|
-
return await serialized(key, () => serializedByKeys(remainingKeys, callback));
|
|
22
|
-
}
|
|
23
18
|
function getParentPath(path) {
|
|
24
19
|
const lastSeparator = path.lastIndexOf("/");
|
|
25
20
|
return lastSeparator < 0 ? "" : path.slice(0, lastSeparator);
|
|
@@ -1200,7 +1195,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
1200
1195
|
let failed = false;
|
|
1201
1196
|
let failure;
|
|
1202
1197
|
try {
|
|
1203
|
-
result = await
|
|
1198
|
+
result = await withLocks(lockKeys, async () => {
|
|
1204
1199
|
for (const id of uniqueIds) this.deferredConflictChecks.set(id, pending);
|
|
1205
1200
|
try {
|
|
1206
1201
|
return await callback(documentIds.length === 2 && documentIds[0] === documentIds[1]);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ConflictService } from "../../base/ConflictService.js";
|
|
2
2
|
import type { ServiceContext } from "../../base/ServiceBase.js";
|
|
3
3
|
export declare class InjectableConflictService<T extends ServiceContext> extends ConflictService<T> {
|
|
4
|
-
queueCheckForIfOpen: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../
|
|
5
|
-
queueCheckFor: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../
|
|
4
|
+
queueCheckForIfOpen: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../conflicts.js").FilePathWithPrefix) => Promise<void>, unknown>;
|
|
5
|
+
queueCheckFor: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../conflicts.js").FilePathWithPrefix) => Promise<void>, unknown>;
|
|
6
6
|
ensureAllProcessed: import("../../lib/HandlerUtils.js").HandlerFunction<() => Promise<boolean>, unknown>;
|
|
7
|
-
resolveByDeletingRevision: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../
|
|
8
|
-
resolve: import("../../lib/HandlerUtils.js").HandlerFunction<(filename: import("../../../
|
|
9
|
-
resolveByNewest: import("../../lib/HandlerUtils.js").HandlerFunction<(filename: import("../../../
|
|
7
|
+
resolveByDeletingRevision: import("../../lib/HandlerUtils.js").HandlerFunction<(path: import("../../../conflicts.js").FilePathWithPrefix, deleteRevision: string, title: string) => Promise<typeof import("../../../common/types.js").MISSING_OR_ERROR | typeof import("../../../common/types.js").AUTO_MERGED>, unknown>;
|
|
8
|
+
resolve: import("../../lib/HandlerUtils.js").HandlerFunction<(filename: import("../../../conflicts.js").FilePathWithPrefix) => Promise<void>, unknown>;
|
|
9
|
+
resolveByNewest: import("../../lib/HandlerUtils.js").HandlerFunction<(filename: import("../../../conflicts.js").FilePathWithPrefix) => Promise<boolean>, unknown>;
|
|
10
10
|
resolveAllConflictedFilesByNewerOnes: import("../../lib/HandlerUtils.js").HandlerFunction<() => Promise<void>, unknown>;
|
|
11
11
|
}
|
|
@@ -94,11 +94,12 @@ export declare class Binder<T extends (...args: any[]) => any> implements Binder
|
|
|
94
94
|
*/
|
|
95
95
|
constructor(name: string, initialCallback?: T);
|
|
96
96
|
private _callback;
|
|
97
|
+
private _registrationToken;
|
|
97
98
|
/**
|
|
98
99
|
* Assigns a new handler function.
|
|
99
100
|
* @param callback The new handler function to assign.
|
|
100
101
|
*/
|
|
101
|
-
assign(callback: T, override?: boolean):
|
|
102
|
+
assign(callback: T, override?: boolean): UnregisterFunction;
|
|
102
103
|
/**
|
|
103
104
|
* Invokes the assigned handler function with the provided arguments.
|
|
104
105
|
* @param args The arguments to pass to the handler function.
|
|
@@ -114,13 +115,14 @@ export declare class LazyBinder<T extends (...args: any[]) => any> implements Bi
|
|
|
114
115
|
private _name;
|
|
115
116
|
private _callbackPromise;
|
|
116
117
|
private _callback;
|
|
118
|
+
private _registrationToken;
|
|
117
119
|
/**
|
|
118
120
|
* Creates a new LazyBinder instance.
|
|
119
121
|
* @param name The name of the handler.
|
|
120
122
|
* @param initialCallback An optional initial callback function to assign.
|
|
121
123
|
*/
|
|
122
124
|
constructor(name: string, initialCallback?: T);
|
|
123
|
-
assign(callback: T, override?: boolean):
|
|
125
|
+
assign(callback: T, override?: boolean): UnregisterFunction;
|
|
124
126
|
/**
|
|
125
127
|
* Invokes the assigned handler function with the provided arguments.
|
|
126
128
|
* @param args The arguments to pass to the handler function.
|
|
@@ -300,9 +302,9 @@ export interface HandlerFunction<TFunc extends (...args: any[]) => U | Promise<U
|
|
|
300
302
|
* Assigns a new handler function.
|
|
301
303
|
* @param callback The new handler function to assign.
|
|
302
304
|
* @param override Whether to override the existing handler if one is already assigned.
|
|
303
|
-
* @returns A function
|
|
305
|
+
* @returns A function that unregisters this registration if it is still current.
|
|
304
306
|
*/
|
|
305
|
-
setHandler: (callback: TFunc, override?: boolean) =>
|
|
307
|
+
setHandler: (callback: TFunc, override?: boolean) => UnregisterFunction;
|
|
306
308
|
}
|
|
307
309
|
/**
|
|
308
310
|
* A function type that can be used as a handler with assignable functionality.
|
|
@@ -316,9 +318,9 @@ export interface LazyHandlerFunction<TFunc extends (...args: any[]) => U | Promi
|
|
|
316
318
|
* Assigns a new handler function.
|
|
317
319
|
* @param callback The new handler function to assign.
|
|
318
320
|
* @param override Whether to override the existing handler if one is already assigned.
|
|
319
|
-
* @returns A function
|
|
321
|
+
* @returns A function that unregisters this registration if it is still current.
|
|
320
322
|
*/
|
|
321
|
-
setHandler: (callback: TFunc, override?: boolean) =>
|
|
323
|
+
setHandler: (callback: TFunc, override?: boolean) => UnregisterFunction;
|
|
322
324
|
}
|
|
323
325
|
/**
|
|
324
326
|
* A function type that can be used as a multiple handler with add/remove functionality.
|