@vrtmrz/livesync-commonlib 0.1.0 → 0.1.1-rc.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 +2 -2
- package/dist/API/DirectFileManipulator.d.ts +1 -1
- package/dist/API/DirectFileManipulatorV2.d.ts +16 -31
- package/dist/API/DirectFileManipulatorV2.js +35 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/managers/LiveSyncManagers.js +1 -1
- package/dist/services/HeadlessServices.d.ts +4 -0
- package/dist/services/HeadlessServices.js +7 -3
- package/dist/services/base/KeyValueDBService.d.ts +1 -0
- package/dist/services/base/KeyValueDBService.js +7 -5
- package/dist/services/base/PathService.d.ts +6 -0
- package/dist/services/base/PathService.js +4 -1
- package/dist/services/base/ReplicatorService.d.ts +1 -0
- package/dist/services/base/ReplicatorService.js +8 -6
- package/dist/services/implements/base/SvelteDialog.d.ts +5 -0
- package/dist/services/implements/headless/HeadlessAPIService.js +2 -0
- package/docs/proven-in-use.md +14 -6
- package/package.json +6 -31
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Self-hosted LiveSync Commonlib
|
|
2
2
|
|
|
3
|
-
Commonlib is the ESM package which provides shared data, storage, replication, and host-context primitives for Self-hosted LiveSync, its CLI,
|
|
3
|
+
Commonlib is the ESM package which provides shared data, storage, replication, and host-context primitives for Self-hosted LiveSync, its CLI, WebApp, WebPeer, and LiveSync Bridge.
|
|
4
4
|
|
|
5
5
|
This package is primarily for maintainers of those clients and for integrations which deliberately reuse a reviewed Commonlib contract. It is not yet a general-purpose LiveSync SDK: the package does not provide a stable end-to-end client factory or own host UI, permissions, credentials, process lifecycle, or application persistence.
|
|
6
6
|
|
|
@@ -127,7 +127,7 @@ Package developers should read [the developer guide](docs/development.md). The f
|
|
|
127
127
|
|
|
128
128
|
## Proven in maintained hosts
|
|
129
129
|
|
|
130
|
-
The package boundary is exercised by Self-hosted LiveSync's Obsidian plug-in, CLI,
|
|
130
|
+
The package boundary is exercised by Self-hosted LiveSync's Obsidian plug-in, CLI, WebApp, and WebPeer compositions. Their validation is not uniform: the Obsidian plug-in and CLI have the strongest device and long-running synchronisation evidence, while WebApp and WebPeer have focused adapter and runtime tests, browser smoke tests, a cross-application P2P scenario ending at the production CLI, and public Pages smoke validation.
|
|
131
131
|
|
|
132
132
|
See [Proven in maintained hosts](docs/proven-in-use.md) for the actual composition points, downstream tests, and remaining limits. The examples deliberately separate focused public entries which a new consumer may copy from `compat/*` imports which exist to migrate the current application.
|
|
133
133
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { DirectFileManipulator } from "./DirectFileManipulatorV2.js";
|
|
2
|
-
export type { DirectFileManipulatorOptions } from "./DirectFileManipulatorV2.js";
|
|
2
|
+
export type { DirectFileManipulatorOptions, DirectFileManipulatorRuntimeOptions, } from "./DirectFileManipulatorV2.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The API for manipulating files stored in the CouchDB by Self-hosted LiveSync or its families.
|
|
3
3
|
*/
|
|
4
|
-
import { type DocumentID, type FilePathWithPrefix, type EntryHasPath, type FilePath, type EntryDoc, type NewEntry, type PlainEntry, type LoadedEntry, type HashAlgorithm, type RemoteDBSettings, type ChunkSplitterVersion, type SyncParameters, type E2EEAlgorithm } from "../common/types.js";
|
|
4
|
+
import { type DocumentID, type FilePathWithPrefix, type EntryHasPath, type FilePath, type EntryDoc, type NewEntry, type PlainEntry, type LoadedEntry, type HashAlgorithm, type ObsidianLiveSyncSettings, type RemoteDBSettings, type ChunkSplitterVersion, type SyncParameters, type E2EEAlgorithm } from "../common/types.js";
|
|
5
5
|
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "../pouchdb/LiveSyncLocalDB.js";
|
|
6
6
|
import { HeadlessServiceHub } from "../services/HeadlessServices.js";
|
|
7
7
|
import { HeadlessDatabaseService } from "../services/implements/headless/HeadlessDatabaseService.js";
|
|
@@ -34,6 +34,11 @@ export type DirectFileManipulatorOptions = {
|
|
|
34
34
|
chunkSplitterVersion?: ChunkSplitterVersion;
|
|
35
35
|
E2EEAlgorithm?: E2EEAlgorithm;
|
|
36
36
|
};
|
|
37
|
+
/** Host runtime capabilities used by a direct CouchDB connection. */
|
|
38
|
+
export type DirectFileManipulatorRuntimeOptions = Readonly<{
|
|
39
|
+
/** Fetch implementation passed to PouchDB. Omit it to use PouchDB's default transport. */
|
|
40
|
+
fetch?: typeof globalThis.fetch;
|
|
41
|
+
}>;
|
|
37
42
|
export type ReadyEntry = (NewEntry | PlainEntry) & {
|
|
38
43
|
data: string[];
|
|
39
44
|
};
|
|
@@ -54,6 +59,7 @@ export type EnumerateConditions = {
|
|
|
54
59
|
export declare class DirectFileManipulator implements LiveSyncLocalDBEnv {
|
|
55
60
|
liveSyncLocalDB: LiveSyncLocalDB;
|
|
56
61
|
options: DirectFileManipulatorOptions;
|
|
62
|
+
readonly runtimeOptions: DirectFileManipulatorRuntimeOptions;
|
|
57
63
|
ready: {
|
|
58
64
|
promise: Promise<void>;
|
|
59
65
|
resolve: (value: void | PromiseLike<void>) => void;
|
|
@@ -61,8 +67,8 @@ export declare class DirectFileManipulator implements LiveSyncLocalDBEnv {
|
|
|
61
67
|
};
|
|
62
68
|
services: HeadlessServiceHub<ServiceContext>;
|
|
63
69
|
init(): Promise<void>;
|
|
64
|
-
getBoundDatabaseService(options: () => DirectFileManipulatorOptions): typeof HeadlessDatabaseService;
|
|
65
|
-
constructor(options: DirectFileManipulatorOptions);
|
|
70
|
+
getBoundDatabaseService(options: () => DirectFileManipulatorOptions, runtimeOptions: DirectFileManipulatorRuntimeOptions): typeof HeadlessDatabaseService;
|
|
71
|
+
constructor(options: DirectFileManipulatorOptions, runtimeOptions?: DirectFileManipulatorRuntimeOptions);
|
|
66
72
|
$$id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix;
|
|
67
73
|
$$path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID>;
|
|
68
74
|
getInitialSyncParameters(setting: RemoteDBSettings): Promise<SyncParameters>;
|
|
@@ -73,7 +79,7 @@ export declare class DirectFileManipulator implements LiveSyncLocalDBEnv {
|
|
|
73
79
|
getSettings(): RemoteDBSettings;
|
|
74
80
|
close(): Promise<void>;
|
|
75
81
|
path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID>;
|
|
76
|
-
get settings():
|
|
82
|
+
get settings(): ObsidianLiveSyncSettings;
|
|
77
83
|
/**
|
|
78
84
|
* Get specific document from the Remote Database by path.
|
|
79
85
|
* @param path
|
|
@@ -103,7 +109,7 @@ export declare class DirectFileManipulator implements LiveSyncLocalDBEnv {
|
|
|
103
109
|
enumerate(_cond: EnumerateConditions): AsyncGenerator<never, void, unknown>;
|
|
104
110
|
_enumerate(startKey: string, endKey: string, opt: {
|
|
105
111
|
metaOnly: boolean;
|
|
106
|
-
}): AsyncGenerator<
|
|
112
|
+
}): AsyncGenerator<(import("../common/types.js").DatabaseEntry & import("../common/types.js").EntryBase & import("../common/types.js").EntryWithEden & {
|
|
107
113
|
path: FilePathWithPrefix;
|
|
108
114
|
children: string[];
|
|
109
115
|
type: import("../common/models/db.type.js").EntryTypes["NOTE_BINARY"];
|
|
@@ -111,39 +117,18 @@ export declare class DirectFileManipulator implements LiveSyncLocalDBEnv {
|
|
|
111
117
|
path: FilePathWithPrefix;
|
|
112
118
|
children: string[];
|
|
113
119
|
type: import("../common/models/db.type.js").EntryTypes["NOTE_PLAIN"];
|
|
114
|
-
} & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) |
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} & {
|
|
119
|
-
deleted?: boolean;
|
|
120
|
-
} & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (import("../common/types.js").DatabaseEntry & import("../common/types.js").EntryBase & import("../common/types.js").EntryWithEden & {
|
|
120
|
+
} & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | ReadyEntry, void, unknown>;
|
|
121
|
+
enumerateAllNormalDocs(opt: {
|
|
122
|
+
metaOnly: boolean;
|
|
123
|
+
}): AsyncGenerator<(import("../common/types.js").DatabaseEntry & import("../common/types.js").EntryBase & import("../common/types.js").EntryWithEden & {
|
|
121
124
|
path: FilePathWithPrefix;
|
|
122
125
|
children: string[];
|
|
123
126
|
type: import("../common/models/db.type.js").EntryTypes["NOTE_BINARY"];
|
|
124
|
-
} & {
|
|
125
|
-
data: string | string[];
|
|
126
|
-
datatype: import("../common/types.js").EntryTypeNotes;
|
|
127
127
|
} & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (import("../common/types.js").DatabaseEntry & import("../common/types.js").EntryBase & import("../common/types.js").EntryWithEden & {
|
|
128
128
|
path: FilePathWithPrefix;
|
|
129
129
|
children: string[];
|
|
130
130
|
type: import("../common/models/db.type.js").EntryTypes["NOTE_PLAIN"];
|
|
131
|
-
} &
|
|
132
|
-
data: string | string[];
|
|
133
|
-
datatype: import("../common/types.js").EntryTypeNotes;
|
|
134
|
-
} & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (import("../common/types.js").DatabaseEntry & import("../common/types.js").EntryBase & import("../common/types.js").EntryWithEden & {
|
|
135
|
-
path: FilePathWithPrefix;
|
|
136
|
-
children: string[];
|
|
137
|
-
type: import("../common/models/db.type.js").EntryTypes["NOTE_BINARY"];
|
|
138
|
-
} & {
|
|
139
|
-
deleted?: boolean;
|
|
140
|
-
} & {
|
|
141
|
-
data: string | string[];
|
|
142
|
-
datatype: import("../common/types.js").EntryTypeNotes;
|
|
143
|
-
} & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta), void, unknown>, unknown>;
|
|
144
|
-
enumerateAllNormalDocs(opt: {
|
|
145
|
-
metaOnly: boolean;
|
|
146
|
-
}): AsyncGenerator<ReadyEntry, void, unknown>;
|
|
131
|
+
} & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | ReadyEntry, void, unknown>;
|
|
147
132
|
watching: boolean;
|
|
148
133
|
changes: PouchDB.Core.Changes<EntryDoc> | undefined;
|
|
149
134
|
since: string;
|
|
@@ -42,32 +42,45 @@ function isReadyEntry(doc) {
|
|
|
42
42
|
class DirectFileManipulator {
|
|
43
43
|
liveSyncLocalDB;
|
|
44
44
|
options;
|
|
45
|
+
runtimeOptions;
|
|
45
46
|
ready = promiseWithResolvers();
|
|
46
47
|
services;
|
|
47
48
|
async init() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
try {
|
|
50
|
+
await this.services.appLifecycle.onReady();
|
|
51
|
+
await this.liveSyncLocalDB.initializeDatabase();
|
|
52
|
+
this.liveSyncLocalDB.refreshSettings();
|
|
53
|
+
this.ready.resolve();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
this.ready.reject(error);
|
|
56
|
+
}
|
|
52
57
|
}
|
|
53
|
-
getBoundDatabaseService(options) {
|
|
58
|
+
getBoundDatabaseService(options, runtimeOptions) {
|
|
54
59
|
const _option = options;
|
|
60
|
+
const _runtimeOptions = runtimeOptions;
|
|
55
61
|
return class HeadlessDatabaseServiceExt extends HeadlessDatabaseService {
|
|
56
62
|
createPouchDBInstance(_name, _options) {
|
|
57
63
|
const option = _option();
|
|
58
|
-
|
|
64
|
+
const pouchDBOptions = {
|
|
59
65
|
auth: { username: option.username, password: option.password }
|
|
60
|
-
}
|
|
66
|
+
};
|
|
67
|
+
if (_runtimeOptions.fetch) {
|
|
68
|
+
pouchDBOptions.fetch = _runtimeOptions.fetch;
|
|
69
|
+
}
|
|
70
|
+
return new PouchDB(option.url + "/" + option.database, pouchDBOptions);
|
|
61
71
|
}
|
|
62
72
|
};
|
|
63
73
|
}
|
|
64
|
-
constructor(options) {
|
|
74
|
+
constructor(options, runtimeOptions = {}) {
|
|
65
75
|
this.options = options;
|
|
76
|
+
this.runtimeOptions = runtimeOptions;
|
|
66
77
|
const getSettings = () => this.settings;
|
|
67
78
|
const context = new ServiceContext();
|
|
68
79
|
this.services = new HeadlessServiceHub(context, {
|
|
69
80
|
pouchDB: PouchDB,
|
|
70
|
-
database: this.getBoundDatabaseService(() => this.options)
|
|
81
|
+
database: this.getBoundDatabaseService(() => this.options, this.runtimeOptions),
|
|
82
|
+
getPathObfuscationPassphrase: () => this.options.obfuscatePassphrase ?? false,
|
|
83
|
+
databaseLifecycleMode: "direct-access"
|
|
71
84
|
});
|
|
72
85
|
this.services.setting.saveData.setHandler((data) => {
|
|
73
86
|
console.warn("Saving settings is not supported in DirectFileManipulator.");
|
|
@@ -77,8 +90,11 @@ class DirectFileManipulator {
|
|
|
77
90
|
console.warn("Loading settings is not supported in DirectFileManipulator.");
|
|
78
91
|
return Promise.resolve(getSettings());
|
|
79
92
|
});
|
|
93
|
+
this.services.setting.settings = getSettings();
|
|
80
94
|
this.services.databaseEvents.onDatabaseInitialisation.addHandler(this.$everyOnInitializeDatabase.bind(this));
|
|
81
95
|
this.liveSyncLocalDB = new LiveSyncLocalDB(this.options.url, this);
|
|
96
|
+
void this.ready.promise.catch(() => {
|
|
97
|
+
});
|
|
82
98
|
void this.init();
|
|
83
99
|
}
|
|
84
100
|
$$id2path(id, entry, stripPrefix) {
|
|
@@ -190,6 +206,7 @@ class DirectFileManipulator {
|
|
|
190
206
|
endpoint: "",
|
|
191
207
|
enableCompression: this.options.enableCompression ?? DEFAULT_SETTINGS.enableCompression,
|
|
192
208
|
handleFilenameCaseSensitive: this.options.handleFilenameCaseSensitive ?? DEFAULT_SETTINGS.handleFilenameCaseSensitive,
|
|
209
|
+
usePathObfuscation: !!this.options.obfuscatePassphrase,
|
|
193
210
|
E2EEAlgorithm: this.options.E2EEAlgorithm ?? E2EEAlgorithms.V2
|
|
194
211
|
}
|
|
195
212
|
};
|
|
@@ -289,7 +306,7 @@ class DirectFileManipulator {
|
|
|
289
306
|
async *enumerate(_cond) {
|
|
290
307
|
}
|
|
291
308
|
async *_enumerate(startKey, endKey, opt) {
|
|
292
|
-
if (opt.metaOnly) return this.liveSyncLocalDB.findEntries(startKey, endKey, {});
|
|
309
|
+
if (opt.metaOnly) return yield* this.liveSyncLocalDB.findEntries(startKey, endKey, {});
|
|
293
310
|
for await (const f of this.liveSyncLocalDB.findEntries(startKey, endKey, {})) {
|
|
294
311
|
yield await this.getByMeta(f);
|
|
295
312
|
}
|
|
@@ -337,7 +354,14 @@ class DirectFileManipulator {
|
|
|
337
354
|
}
|
|
338
355
|
}
|
|
339
356
|
Logger(`WATCH: PROCESSING: ${doc.path}`, LEVEL_VERBOSE, "watch");
|
|
340
|
-
|
|
357
|
+
let docX;
|
|
358
|
+
try {
|
|
359
|
+
docX = await this.getByMeta(doc);
|
|
360
|
+
} catch (ex) {
|
|
361
|
+
Logger(`WATCH: DOCUMENT LOAD FAILED: ${doc.path}`, LEVEL_INFO, "watch");
|
|
362
|
+
Logger(ex, LEVEL_VERBOSE, "watch");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
341
365
|
try {
|
|
342
366
|
await callback(docX, change.seq);
|
|
343
367
|
Logger(`WATCH: PROCESS DONE: ${doc.path}`, LEVEL_INFO, "watch");
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { DirectFileManipulator, type DirectFileManipulatorOptions } from "./API/DirectFileManipulator.js";
|
|
1
|
+
export { DirectFileManipulator, type DirectFileManipulatorOptions, type DirectFileManipulatorRuntimeOptions, } from "./API/DirectFileManipulator.js";
|
package/dist/index.js
CHANGED
|
@@ -56,7 +56,7 @@ class LiveSyncManagers {
|
|
|
56
56
|
}
|
|
57
57
|
getManagerMembers() {
|
|
58
58
|
this.log("Creating LiveSync Managers...");
|
|
59
|
-
const database = this.options.
|
|
59
|
+
const database = this.options.database;
|
|
60
60
|
const changeManager = new ChangeManager({
|
|
61
61
|
database
|
|
62
62
|
});
|
|
@@ -12,5 +12,9 @@ export declare class HeadlessServiceHub<T extends ServiceContext> extends Inject
|
|
|
12
12
|
database?: Constructor<DatabaseService<T>>;
|
|
13
13
|
openKeyValueDatabase?: KeyValueDatabaseFactory;
|
|
14
14
|
onDisplayLanguageChanged?: (language: ObsidianLiveSyncSettings["displayLanguage"]) => void;
|
|
15
|
+
/** Optional host capability when path obfuscation uses a secret distinct from content encryption. */
|
|
16
|
+
getPathObfuscationPassphrase?: () => string | false;
|
|
17
|
+
/** Direct database clients do not run application-level replication or key-value database lifecycles. */
|
|
18
|
+
databaseLifecycleMode?: "application" | "direct-access";
|
|
15
19
|
});
|
|
16
20
|
}
|
|
@@ -50,6 +50,7 @@ class HeadlessUIService extends UIService {
|
|
|
50
50
|
class HeadlessServiceHub extends InjectableServiceHub {
|
|
51
51
|
constructor(_context, overrideServiceConstructor) {
|
|
52
52
|
const context = _context ?? new ServiceContext();
|
|
53
|
+
const registerApplicationDatabaseLifecycle = (overrideServiceConstructor.databaseLifecycleMode ?? "application") === "application";
|
|
53
54
|
const API = new HeadlessAPIService(context);
|
|
54
55
|
const conflict = new InjectableConflictService(context);
|
|
55
56
|
const fileProcessing = new InjectableFileProcessingService(context);
|
|
@@ -74,7 +75,8 @@ class HeadlessServiceHub extends InjectableServiceHub {
|
|
|
74
75
|
const test = new InjectableTestService(context);
|
|
75
76
|
const databaseEvents = new InjectableDatabaseEventService(context);
|
|
76
77
|
const path = new PathServiceCompat(context, {
|
|
77
|
-
settingService: setting
|
|
78
|
+
settingService: setting,
|
|
79
|
+
getPathObfuscationPassphrase: overrideServiceConstructor.getPathObfuscationPassphrase
|
|
78
80
|
});
|
|
79
81
|
const database = new (overrideServiceConstructor.database ?? HeadlessDatabaseService)(context, {
|
|
80
82
|
pouchDB: overrideServiceConstructor.pouchDB,
|
|
@@ -90,7 +92,8 @@ class HeadlessServiceHub extends InjectableServiceHub {
|
|
|
90
92
|
const replicator = new InjectableReplicatorService(context, {
|
|
91
93
|
settingService: setting,
|
|
92
94
|
appLifecycleService: appLifecycle,
|
|
93
|
-
databaseEventService: databaseEvents
|
|
95
|
+
databaseEventService: databaseEvents,
|
|
96
|
+
registerLifecycleHandlers: registerApplicationDatabaseLifecycle
|
|
94
97
|
});
|
|
95
98
|
const replication = new InjectableReplicationService(context, {
|
|
96
99
|
APIService: API,
|
|
@@ -104,7 +107,8 @@ class HeadlessServiceHub extends InjectableServiceHub {
|
|
|
104
107
|
openKeyValueDatabase: overrideServiceConstructor.openKeyValueDatabase ?? createIndexedDBKeyValueDatabaseFactory(),
|
|
105
108
|
appLifecycle,
|
|
106
109
|
databaseEvents,
|
|
107
|
-
vault
|
|
110
|
+
vault,
|
|
111
|
+
registerLifecycleHandlers: registerApplicationDatabaseLifecycle
|
|
108
112
|
});
|
|
109
113
|
const control = new ControlService(context, {
|
|
110
114
|
appLifecycleService: appLifecycle,
|
|
@@ -9,6 +9,7 @@ export interface KeyValueDBDependencies<T extends ServiceContext = ServiceContex
|
|
|
9
9
|
databaseEvents: InjectableDatabaseEventService<T>;
|
|
10
10
|
vault: IVaultService;
|
|
11
11
|
appLifecycle: AppLifecycleServiceBase<T>;
|
|
12
|
+
registerLifecycleHandlers?: boolean;
|
|
12
13
|
}
|
|
13
14
|
/**
|
|
14
15
|
* The KeyValueDBService provides methods for managing the local key-value database.
|
|
@@ -93,11 +93,13 @@ class KeyValueDBService extends ServiceBase {
|
|
|
93
93
|
this.vault = dependencies.vault;
|
|
94
94
|
this.appLifecycle = dependencies.appLifecycle;
|
|
95
95
|
this.openKeyValueDatabase = dependencies.openKeyValueDatabase;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
if (dependencies.registerLifecycleHandlers ?? true) {
|
|
97
|
+
this.databaseEvents.onResetDatabase.addHandler(this._everyOnResetDatabase.bind(this));
|
|
98
|
+
this.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
|
|
99
|
+
this.databaseEvents.onDatabaseInitialisation.addHandler(this._everyOnInitializeDatabase.bind(this));
|
|
100
|
+
this.databaseEvents.onUnloadDatabase.addHandler(this._onOtherDatabaseUnload.bind(this));
|
|
101
|
+
this.databaseEvents.onCloseDatabase.addHandler(this._onOtherDatabaseClose.bind(this));
|
|
102
|
+
}
|
|
101
103
|
}
|
|
102
104
|
openSimpleStore(kind) {
|
|
103
105
|
const getDB = () => {
|
|
@@ -4,6 +4,11 @@ import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
|
|
|
4
4
|
import type { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "../../common/models/shared.const.symbols.js";
|
|
5
5
|
export interface PathServiceDependencies {
|
|
6
6
|
settingService: ISettingService;
|
|
7
|
+
/**
|
|
8
|
+
* Optional host capability for consumers which keep content encryption and
|
|
9
|
+
* path-obfuscation passphrases separate.
|
|
10
|
+
*/
|
|
11
|
+
getPathObfuscationPassphrase?: () => string | false;
|
|
7
12
|
}
|
|
8
13
|
/**
|
|
9
14
|
* The PathService provides methods for converting between file paths and document IDs.
|
|
@@ -11,6 +16,7 @@ export interface PathServiceDependencies {
|
|
|
11
16
|
*/
|
|
12
17
|
export declare abstract class PathService<T extends ServiceContext = ServiceContext> extends ServiceBase<T> implements IPathService {
|
|
13
18
|
protected settingService: ISettingService;
|
|
19
|
+
private readonly getPathObfuscationPassphrase?;
|
|
14
20
|
protected abstract normalizePath(path: string): string;
|
|
15
21
|
get settings(): import("../../common/types.js").ObsidianLiveSyncSettings;
|
|
16
22
|
constructor(context: T, dependencies: PathServiceDependencies);
|
|
@@ -3,12 +3,14 @@ import { addPrefix, id2path_base, path2id_base } from "../../string_and_binary/p
|
|
|
3
3
|
import { isInternalMetadata, stripInternalMetadataPrefix } from "../../common/typeUtils.js";
|
|
4
4
|
class PathService extends ServiceBase {
|
|
5
5
|
settingService;
|
|
6
|
+
getPathObfuscationPassphrase;
|
|
6
7
|
get settings() {
|
|
7
8
|
return this.settingService.currentSettings();
|
|
8
9
|
}
|
|
9
10
|
constructor(context, dependencies) {
|
|
10
11
|
super(context);
|
|
11
12
|
this.settingService = dependencies.settingService;
|
|
13
|
+
this.getPathObfuscationPassphrase = dependencies.getPathObfuscationPassphrase;
|
|
12
14
|
}
|
|
13
15
|
_id2path(id, entry) {
|
|
14
16
|
const filename = id2path_base(id, entry);
|
|
@@ -51,9 +53,10 @@ class PathService extends ServiceBase {
|
|
|
51
53
|
async path2id(filename, prefix) {
|
|
52
54
|
const destPath = addPrefix(filename, prefix ?? "");
|
|
53
55
|
const setting = this.settings;
|
|
56
|
+
const pathObfuscationPassphrase = this.getPathObfuscationPassphrase?.() ?? (setting.usePathObfuscation ? setting.passphrase : false);
|
|
54
57
|
return await this._path2id(
|
|
55
58
|
destPath,
|
|
56
|
-
|
|
59
|
+
pathObfuscationPassphrase,
|
|
57
60
|
!setting.handleFilenameCaseSensitive
|
|
58
61
|
);
|
|
59
62
|
}
|
|
@@ -11,6 +11,7 @@ export interface ReplicatorServiceDependencies {
|
|
|
11
11
|
appLifecycleService: AppLifecycleService;
|
|
12
12
|
databaseEventService: DatabaseEventService;
|
|
13
13
|
activityRunner?: AsyncActivityRunner;
|
|
14
|
+
registerLifecycleHandlers?: boolean;
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* The ReplicatorService provides methods for managing replication.
|
|
@@ -17,13 +17,15 @@ class ReplicatorService extends ServiceBase {
|
|
|
17
17
|
this.appLifecycleService = dependencies.appLifecycleService;
|
|
18
18
|
this._unresolvedErrorManager = new UnresolvedErrorManager(dependencies.appLifecycleService, this.context.events);
|
|
19
19
|
this.settingService = dependencies.settingService;
|
|
20
|
-
this.settingService.onRealiseSetting.addHandler(this._initialiseReplicator.bind(this));
|
|
21
20
|
this.databaseEventService = dependencies.databaseEventService;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
if (dependencies.registerLifecycleHandlers ?? true) {
|
|
22
|
+
this.settingService.onRealiseSetting.addHandler(this._initialiseReplicator.bind(this));
|
|
23
|
+
this.databaseEventService.onResetDatabase.addHandler(this.disposeReplicator.bind(this));
|
|
24
|
+
this.databaseEventService.onDatabaseInitialisation.addHandler(this.disposeReplicator.bind(this));
|
|
25
|
+
this.databaseEventService.onDatabaseInitialised.addHandler(this.reinitialiseReplicator.bind(this));
|
|
26
|
+
this.databaseEventService.onDatabaseHasReady.addHandler(this.reinitialiseReplicator.bind(this));
|
|
27
|
+
this.appLifecycleService.onSuspending.addHandler(this.suspendReplication.bind(this));
|
|
28
|
+
}
|
|
27
29
|
}
|
|
28
30
|
dependencies;
|
|
29
31
|
boundedRemoteActivityCount = reactiveSource(0);
|
|
@@ -66,6 +66,11 @@ export interface SvelteDialogInstance<T, U, C extends ServiceContext = ServiceCo
|
|
|
66
66
|
export type SvelteDialogClass<TBase extends Constructor<IModalBase>> = {
|
|
67
67
|
new <T, U, C extends ServiceContext = ServiceContext>(...args: ConstructorParameters<TBase>): SvelteDialogInstance<T, U, C>;
|
|
68
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Adds Svelte dialogue state to a modal class through dynamic inheritance.
|
|
71
|
+
*
|
|
72
|
+
* @deprecated Compose the dialogue lifecycle explicitly in the owning host.
|
|
73
|
+
*/
|
|
69
74
|
export declare function SvelteDialogMixIn<TBase extends Constructor<IModalBase>>(TBase: TBase, d: Component<DialogHostProps>): SvelteDialogClass<TBase>;
|
|
70
75
|
export declare abstract class SvelteDialogManagerBase<T extends ServiceContext> implements SvelteDialogManager<T> {
|
|
71
76
|
abstract openSvelteDialog<T, U = T>(component: ComponentHasResult<T, U>, initialData?: U): Promise<T | undefined>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { InjectableAPIService } from "../injectable/InjectableAPIService.js";
|
|
2
2
|
import module from "node:crypto";
|
|
3
3
|
import { _activeDocument } from "../../../common/coreEnvFunctions.js";
|
|
4
|
+
import { Logger } from "../../../common/logger.js";
|
|
4
5
|
class HeadlessConfirm {
|
|
5
6
|
askYesNo(message) {
|
|
6
7
|
console.error(`[Headless] ${message} \u2192 no`);
|
|
@@ -37,6 +38,7 @@ class HeadlessAPIService extends InjectableAPIService {
|
|
|
37
38
|
constructor(context) {
|
|
38
39
|
super(context);
|
|
39
40
|
this._confirmInstance = new HeadlessConfirm();
|
|
41
|
+
this.addLog.setHandler(Logger);
|
|
40
42
|
}
|
|
41
43
|
get confirm() {
|
|
42
44
|
return this._confirmInstance;
|
package/docs/proven-in-use.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Commonlib is extracted from the implementation shared by [Self-hosted LiveSync](https://github.com/vrtmrz/obsidian-livesync) and its host applications. The package is therefore exercised by a substantial maintained consumer, but it does not yet claim a separate ecosystem of independent applications or a stable end-to-end SDK.
|
|
4
4
|
|
|
5
|
-
The useful evidence is host- and contract-specific. Focused Commonlib tests establish reusable result semantics; Self-hosted LiveSync tests establish how those contracts compose into the Obsidian plug-in and CLI. The
|
|
5
|
+
The useful evidence is host- and contract-specific. Focused Commonlib tests establish reusable result semantics; Self-hosted LiveSync tests establish how those contracts compose into the Obsidian plug-in, CLI, WebApp, and WebPeer. The Obsidian plug-in and CLI retain the strongest device and long-running synchronisation evidence. The browser applications add focused adapter and runtime tests, browser smoke tests, a cross-application P2P scenario ending at the production CLI, and public Pages smoke validation.
|
|
6
6
|
|
|
7
7
|
The code paths below identify the maintained Self-hosted LiveSync composition points used to establish this package boundary. The exact reviewed downstream revision and compatibility imports are recorded in [`migration/downstream-imports.json`](migration/downstream-imports.json), which is regenerated by the package-boundary tooling rather than copied into prose links. The integration descriptions must still be reviewed when the maintained downstream changes.
|
|
8
8
|
|
|
@@ -22,6 +22,8 @@ The plug-in's manual onboarding and Remote Databases pane use the focused `/remo
|
|
|
22
22
|
|
|
23
23
|
Commonlib's confirmation contract can also convey a host-neutral vertical action-layout request for long, safety-sensitive choices. Self-hosted LiveSync maps that request to Fancy Kit at its Obsidian boundary. Focused tests check the mapping, and the consumer's real-Obsidian suite checks the resulting controls and mobile touch targets; Commonlib does not own the rendering or CSS.
|
|
24
24
|
|
|
25
|
+
Self-hosted LiveSync's Obsidian and browser dialogue hosts now share an application-owned `SvelteDialogSession` lifecycle. They continue to consume Commonlib's dialogue types and manager base, but no maintained LiveSync path uses `SvelteDialogMixIn`. The dynamic-inheritance helper remains available through the compatibility boundary and is deprecated in favour of host-owned composition.
|
|
26
|
+
|
|
25
27
|
## CLI
|
|
26
28
|
|
|
27
29
|
The CLI extends the same neutral context with its database path and injected `StandardIo` in `src/apps/cli/services/NodeServiceContext.ts`. Its entry point creates the Node standard-I/O adapter once and passes it to command composition, while unit tests provide stream-independent fakes for parsing, prompts, standard output, and standard error.
|
|
@@ -30,20 +32,26 @@ The CLI filesystem composition receives a configured base directory and construc
|
|
|
30
32
|
|
|
31
33
|
The maintained CLI checks include unit tests, Deno-driven CLI E2E, Compose-based compatibility runs, and the CLI-to-real-Obsidian synchronisation scenario in `test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts`. The latter is the strongest current evidence that the CLI and plug-in compose compatible Commonlib behaviour across two hosts.
|
|
32
34
|
|
|
33
|
-
##
|
|
35
|
+
## WebApp
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
WebApp receives an already authorised `FileSystemDirectoryHandle` from its host and constructs `FileSystemAccessStorageAdapter` inside `src/apps/webapp/adapters/FSAPIFileSystemAdapter.ts`. The adapter does not call `showDirectoryPicker()`, choose a directory, or persist the handle. This is the maintained application example for the browser entry's root-ownership rule.
|
|
36
38
|
|
|
37
|
-
The browser and Node storage implementations run through the same Commonlib result contract, and the
|
|
39
|
+
The browser and Node storage implementations run through the same Commonlib result contract, and the WebApp adapter and runtime have focused unit coverage. Application-owned Deno and Chromium checks cover Root selection, runtime start-up, settings, scanning, and P2P controls. The Compose interoperability scenario transfers content from WebApp to WebPeer and then to the production CLI, while the public Pages smoke check covers the deployed Root and configuration flow. This evidence remains narrower than the real-Obsidian and CLI synchronisation suites.
|
|
38
40
|
|
|
39
41
|
## WebPeer and browser service composition
|
|
40
42
|
|
|
41
|
-
The browser service composition in `src/apps/browser/createLiveSyncBrowserServiceHub.ts`
|
|
43
|
+
The browser service composition in `src/apps/browser/createLiveSyncBrowserServiceHub.ts` is owned by Self-hosted LiveSync. It combines Commonlib's injectable service contracts and data implementations with LiveSync-owned browser API, UI, settings persistence, restart policy, and Vault identity. Commonlib therefore does not provide or select a general browser host.
|
|
42
44
|
|
|
43
|
-
The Obsidian plug-in, CLI, and
|
|
45
|
+
The Obsidian plug-in, CLI, WebApp, and WebPeer compose P2P ownership through `useP2PReplicatorFeature`. Commands and views retain its live result object and resolve the current replicator when an action occurs. Downstream Deno and Compose P2P E2E cover headless peer discovery and synchronisation, including the WebApp-to-WebPeer-to-CLI journey. Application-owned Chromium checks cover the browser UI flows, and the public Pages deployment has separate post-deployment smoke validation. The real-Obsidian Setup URI workflow additionally covers first-device local initialisation, one-pass additional-device Fetch, explicit peer approval, Vault reflection, and a two-way note round-trip.
|
|
44
46
|
|
|
45
47
|
This is evidence that the current package boundary can be consumed without the Obsidian host, but it is not yet a recommended standalone Commonlib client factory. The high-level lifecycle, readiness, error, and disposal contract remains future package work after Self-hosted LiveSync 1.0 rather than a gate for that release.
|
|
46
48
|
|
|
49
|
+
## LiveSync Bridge
|
|
50
|
+
|
|
51
|
+
[LiveSync Bridge](https://github.com/vrtmrz/livesync-bridge) uses the root `DirectFileManipulator` entry to access CouchDB directly from Deno. The host supplies its fetch implementation, owns reachability probes, retries, and process health, and waits for the manipulator's readiness result before starting its watch. It does not compose the full LiveSync application Service Hub or use the planned file-client factory.
|
|
52
|
+
|
|
53
|
+
The downstream Deno checks and unit tests run against the installed package. Its Compose integration starts a real CouchDB instance and verifies create, read, update, and deletion between two Bridge peers. This is evidence for the existing direct-access migration path. Commonlib's focused tests separately cover host fetch injection and initialisation failure reporting. This evidence does not make `DirectFileManipulator` a stable high-level SDK contract or establish the semantics still reserved for `createLiveSyncFileClient`.
|
|
54
|
+
|
|
47
55
|
## Verification ownership
|
|
48
56
|
|
|
49
57
|
Commonlib tests the result and lifecycle contracts of the real implementations it publishes. A maintained host tests only its own composition, persistence, presentation, restart, and policy. When a current host test needs isolation, the host may inject the narrow structural capability it consumes and provide a small fake or spy for that test; Commonlib does not publish test-runner-specific mocks or expose its private service graph.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vrtmrz/livesync-commonlib",
|
|
3
|
-
"version": "0.1.0",
|
|
3
|
+
"version": "0.1.1-rc.0",
|
|
4
4
|
"description": "Shared data, storage, and replication primitives for Self-hosted LiveSync clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,11 +71,6 @@
|
|
|
71
71
|
"import": "./dist/API/processSetting.js",
|
|
72
72
|
"default": "./dist/API/processSetting.js"
|
|
73
73
|
},
|
|
74
|
-
"./compat/bureau/bureau": {
|
|
75
|
-
"types": "./dist/bureau/bureau.d.ts",
|
|
76
|
-
"import": "./dist/bureau/bureau.js",
|
|
77
|
-
"default": "./dist/bureau/bureau.js"
|
|
78
|
-
},
|
|
79
74
|
"./compat/common/ConnectionString": {
|
|
80
75
|
"types": "./dist/common/ConnectionString.d.ts",
|
|
81
76
|
"import": "./dist/common/ConnectionString.js",
|
|
@@ -201,16 +196,16 @@
|
|
|
201
196
|
"import": "./dist/interfaces/DatabaseRebuilder.js",
|
|
202
197
|
"default": "./dist/interfaces/DatabaseRebuilder.js"
|
|
203
198
|
},
|
|
204
|
-
"./compat/interfaces/FileReflectionProvenance": {
|
|
205
|
-
"types": "./dist/interfaces/FileReflectionProvenance.d.ts",
|
|
206
|
-
"import": "./dist/interfaces/FileReflectionProvenance.js",
|
|
207
|
-
"default": "./dist/interfaces/FileReflectionProvenance.js"
|
|
208
|
-
},
|
|
209
199
|
"./compat/interfaces/FileHandler": {
|
|
210
200
|
"types": "./dist/interfaces/FileHandler.d.ts",
|
|
211
201
|
"import": "./dist/interfaces/FileHandler.js",
|
|
212
202
|
"default": "./dist/interfaces/FileHandler.js"
|
|
213
203
|
},
|
|
204
|
+
"./compat/interfaces/FileReflectionProvenance": {
|
|
205
|
+
"types": "./dist/interfaces/FileReflectionProvenance.d.ts",
|
|
206
|
+
"import": "./dist/interfaces/FileReflectionProvenance.js",
|
|
207
|
+
"default": "./dist/interfaces/FileReflectionProvenance.js"
|
|
208
|
+
},
|
|
214
209
|
"./compat/interfaces/KeyValueDatabase": {
|
|
215
210
|
"types": "./dist/interfaces/KeyValueDatabase.d.ts",
|
|
216
211
|
"import": "./dist/interfaces/KeyValueDatabase.js",
|
|
@@ -331,16 +326,6 @@
|
|
|
331
326
|
"import": "./dist/replication/trystero/P2PLogCollector.js",
|
|
332
327
|
"default": "./dist/replication/trystero/P2PLogCollector.js"
|
|
333
328
|
},
|
|
334
|
-
"./compat/replication/trystero/P2PReplicatorBase": {
|
|
335
|
-
"types": "./dist/replication/trystero/P2PReplicatorBase.d.ts",
|
|
336
|
-
"import": "./dist/replication/trystero/P2PReplicatorBase.js",
|
|
337
|
-
"default": "./dist/replication/trystero/P2PReplicatorBase.js"
|
|
338
|
-
},
|
|
339
|
-
"./compat/replication/trystero/P2PReplicatorCore": {
|
|
340
|
-
"types": "./dist/replication/trystero/P2PReplicatorCore.d.ts",
|
|
341
|
-
"import": "./dist/replication/trystero/P2PReplicatorCore.js",
|
|
342
|
-
"default": "./dist/replication/trystero/P2PReplicatorCore.js"
|
|
343
|
-
},
|
|
344
329
|
"./compat/replication/trystero/P2PReplicatorPaneCommon": {
|
|
345
330
|
"types": "./dist/replication/trystero/P2PReplicatorPaneCommon.d.ts",
|
|
346
331
|
"import": "./dist/replication/trystero/P2PReplicatorPaneCommon.js",
|
|
@@ -436,11 +421,6 @@
|
|
|
436
421
|
"import": "./dist/serviceModules/adapters/index.js",
|
|
437
422
|
"default": "./dist/serviceModules/adapters/index.js"
|
|
438
423
|
},
|
|
439
|
-
"./compat/services/BrowserServices": {
|
|
440
|
-
"types": "./dist/services/BrowserServices.d.ts",
|
|
441
|
-
"import": "./dist/services/BrowserServices.js",
|
|
442
|
-
"default": "./dist/services/BrowserServices.js"
|
|
443
|
-
},
|
|
444
424
|
"./compat/services/ServiceHub": {
|
|
445
425
|
"types": "./dist/services/ServiceHub.d.ts",
|
|
446
426
|
"import": "./dist/services/ServiceHub.js",
|
|
@@ -516,11 +496,6 @@
|
|
|
516
496
|
"import": "./dist/services/implements/base/UIService.js",
|
|
517
497
|
"default": "./dist/services/implements/base/UIService.js"
|
|
518
498
|
},
|
|
519
|
-
"./compat/services/implements/browser/BrowserAPIService": {
|
|
520
|
-
"types": "./dist/services/implements/browser/BrowserAPIService.d.ts",
|
|
521
|
-
"import": "./dist/services/implements/browser/BrowserAPIService.js",
|
|
522
|
-
"default": "./dist/services/implements/browser/BrowserAPIService.js"
|
|
523
|
-
},
|
|
524
499
|
"./compat/services/implements/browser/ConfigServiceBrowserCompat": {
|
|
525
500
|
"types": "./dist/services/implements/browser/ConfigServiceBrowserCompat.d.ts",
|
|
526
501
|
"import": "./dist/services/implements/browser/ConfigServiceBrowserCompat.js",
|