@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 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, Webapp, and WebPeer.
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, Webapp, and WebPeer compositions. Their validation is not uniform: the Obsidian plug-in and CLI have the strongest real-runtime and cross-host evidence, while the Webapp and WebPeer currently provide focused adapter, type-check, and build evidence.
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(): RemoteDBSettings;
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<ReadyEntry, AsyncGenerator<(import("../common/types.js").DatabaseEntry & import("../common/types.js").EntryBase & import("../common/types.js").EntryWithEden & {
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) | (import("../common/types.js").DatabaseEntry & import("../common/types.js").EntryBase & import("../common/types.js").EntryWithEden & {
115
- path: FilePathWithPrefix;
116
- children: string[];
117
- type: import("../common/models/db.type.js").EntryTypes["NOTE_BINARY"];
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
- await this.services.appLifecycle.onReady();
49
- await this.liveSyncLocalDB.initializeDatabase();
50
- this.ready.resolve();
51
- this.liveSyncLocalDB.refreshSettings();
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
- return new PouchDB(option.url + "/" + option.database, {
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
- const docX = await this.getByMeta(doc);
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
@@ -1,4 +1,6 @@
1
- import { DirectFileManipulator } from "./API/DirectFileManipulator.js";
1
+ import {
2
+ DirectFileManipulator
3
+ } from "./API/DirectFileManipulator.js";
2
4
  export {
3
5
  DirectFileManipulator
4
6
  };
@@ -56,7 +56,7 @@ class LiveSyncManagers {
56
56
  }
57
57
  getManagerMembers() {
58
58
  this.log("Creating LiveSync Managers...");
59
- const database = this.options.databaseService.localDatabase.localDatabase;
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
- this.databaseEvents.onResetDatabase.addHandler(this._everyOnResetDatabase.bind(this));
97
- this.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
98
- this.databaseEvents.onDatabaseInitialisation.addHandler(this._everyOnInitializeDatabase.bind(this));
99
- this.databaseEvents.onUnloadDatabase.addHandler(this._onOtherDatabaseUnload.bind(this));
100
- this.databaseEvents.onCloseDatabase.addHandler(this._onOtherDatabaseClose.bind(this));
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
- setting.usePathObfuscation ? setting.passphrase : "",
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
- this.databaseEventService.onResetDatabase.addHandler(this.disposeReplicator.bind(this));
23
- this.databaseEventService.onDatabaseInitialisation.addHandler(this.disposeReplicator.bind(this));
24
- this.databaseEventService.onDatabaseInitialised.addHandler(this.reinitialiseReplicator.bind(this));
25
- this.databaseEventService.onDatabaseHasReady.addHandler(this.reinitialiseReplicator.bind(this));
26
- this.appLifecycleService.onSuspending.addHandler(this.suspendReplication.bind(this));
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;
@@ -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 Webapp has focused adapter tests, and both the Webapp and WebPeer are type-checked and built, but they do not have an equally current end-to-end gate.
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
- ## Webapp
35
+ ## WebApp
34
36
 
35
- The 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.
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 Webapp adapter has focused unit coverage. The Webapp is also type-checked and built by downstream verification. Its browser E2E has not been kept at the same level as the Obsidian and CLI suites, so the documentation does not treat Webapp integration as equivalent real-runtime evidence.
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` supplies browser-owned API and UI services around a caller-supplied context. WebPeer and the browser applications exercise this compatibility surface through type checks and builds.
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 Webapp compose P2P ownership through `useP2PReplicatorFeature`. Commands and Obsidian views retain its live result object and resolve the current replicator when an action occurs. WebPeer still enters through the deprecated combined compatibility function, which now delegates the same ownership and lifecycle to the focused feature instead of maintaining a second implementation. Downstream Deno and Compose P2P E2E cover headless peer discovery and synchronisation. 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. The Webapp and WebPeer UI surfaces themselves remain build- and type-check evidence rather than equivalent real-runtime gates.
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",