@vrtmrz/livesync-commonlib 0.3.1 → 0.4.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.
Files changed (51) hide show
  1. package/README.md +29 -2
  2. package/dist/managers/EntryManager/EntryManagerImpls.js +1 -1
  3. package/dist/p2p/P2PService.d.ts +10 -1
  4. package/dist/p2p/P2PService.js +25 -2
  5. package/dist/p2p/index.d.ts +4 -1
  6. package/dist/p2p/index.js +2 -0
  7. package/dist/p2p/p2pDiagnostics.d.ts +35 -0
  8. package/dist/p2p/p2pDiagnostics.js +76 -0
  9. package/dist/pouchdb/ReplicatorShim.batch.d.ts +30 -0
  10. package/dist/pouchdb/ReplicatorShim.batch.js +62 -0
  11. package/dist/pouchdb/ReplicatorShim.d.ts +14 -1
  12. package/dist/pouchdb/ReplicatorShim.js +64 -80
  13. package/dist/replication/SyncParamsHandler.d.ts +1 -0
  14. package/dist/replication/SyncParamsHandler.js +5 -1
  15. package/dist/replication/journal/JournalReceiveFailure.d.ts +7 -0
  16. package/dist/replication/journal/JournalReceiveFailure.js +16 -0
  17. package/dist/replication/journal/JournalSyncCore.d.ts +22 -3
  18. package/dist/replication/journal/JournalSyncCore.js +288 -166
  19. package/dist/replication/journal/JournalSyncTypes.d.ts +13 -0
  20. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +6 -1
  21. package/dist/replication/journal/LiveSyncJournalReplicator.js +31 -44
  22. package/dist/replication/journal/journalCompatibility.d.ts +9 -0
  23. package/dist/replication/journal/journalCompatibility.js +58 -0
  24. package/dist/replication/journal/journalReceive.d.ts +23 -0
  25. package/dist/replication/journal/journalReceive.js +44 -0
  26. package/dist/replication/journal/journalRecords.d.ts +10 -0
  27. package/dist/replication/journal/journalRecords.js +96 -0
  28. package/dist/replication/trystero/P2PLogCollector.d.ts +12 -0
  29. package/dist/replication/trystero/P2PLogCollector.js +107 -72
  30. package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +4 -0
  31. package/dist/replication/trystero/P2PRoomSessionOwner.js +39 -2
  32. package/dist/replication/trystero/TrysteroReplicator.d.ts +21 -24
  33. package/dist/replication/trystero/TrysteroReplicator.js +191 -69
  34. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +11 -0
  35. package/dist/replication/trystero/p2pReplicationDiagnostics.d.ts +9 -0
  36. package/dist/replication/trystero/p2pReplicationDiagnostics.js +51 -0
  37. package/dist/replication/trystero/p2pReplicationProgress.d.ts +15 -0
  38. package/dist/replication/trystero/p2pReplicationProgress.js +37 -0
  39. package/dist/replication/trystero/types.d.ts +3 -0
  40. package/dist/services/base/APIService.d.ts +10 -0
  41. package/dist/services/base/APIService.js +13 -0
  42. package/dist/services/base/IService.d.ts +2 -0
  43. package/docs/adr/2026_11_shared_storage_path_resolution.md +4 -2
  44. package/docs/design_docs/storage_path_resolution.md +8 -7
  45. package/docs/development.md +2 -2
  46. package/docs/journal-receive-recovery.md +69 -0
  47. package/docs/p2p-transport-lifecycle.md +117 -10
  48. package/docs/platform-storage.md +2 -2
  49. package/docs/releasing.md +6 -0
  50. package/docs/unicode-path-identity.md +9 -8
  51. package/package.json +2 -2
@@ -0,0 +1,9 @@
1
+ import { type P2PDiagnosticInput } from "../../p2p/p2pDiagnostics.js";
2
+ import type { P2PReplicationResult } from "./TrysteroReplicator.js";
3
+ /** Describe an observed response without coercing an unavailable response to refusal. */
4
+ export declare function authenticationResultDiagnostic(peerId: string, response: unknown): P2PDiagnosticInput;
5
+ export declare function authenticationFailureDiagnostic(peerId: string, error: unknown, signal?: AbortSignal): P2PDiagnosticInput;
6
+ /** Classify the result without changing the operation's result object or error. */
7
+ export declare function pullResultDiagnostic(peerId: string, status: P2PReplicationResult["status"]): P2PDiagnosticInput;
8
+ export declare function pullFailureDiagnostic(peerId: string, cancelled: boolean): P2PDiagnosticInput;
9
+ export declare function notificationFailureDiagnostic(peerId: string, error: unknown): P2PDiagnosticInput;
@@ -0,0 +1,51 @@
1
+ import { classifyP2PRpcFailure } from "../../p2p/p2pDiagnostics.js";
2
+ const authenticationFailureCodes = {
3
+ "rpc-timeout": "auth-timeout",
4
+ "rpc-cancelled": "auth-cancelled",
5
+ "rpc-error": "auth-error"
6
+ };
7
+ const notificationFailureCodes = {
8
+ "rpc-timeout": "change-notification-timeout",
9
+ "rpc-cancelled": "change-notification-cancelled",
10
+ "rpc-error": "change-notification-error"
11
+ };
12
+ function authenticationResultDiagnostic(peerId, response) {
13
+ if (response === true) {
14
+ return { category: "peer", code: "auth-accepted", peerId };
15
+ }
16
+ if (response === false) {
17
+ return { category: "peer", code: "auth-refused", reason: "authentication-refused", peerId };
18
+ }
19
+ return { category: "peer", code: "auth-error", reason: "authentication-unobserved", peerId };
20
+ }
21
+ function authenticationFailureDiagnostic(peerId, error, signal) {
22
+ const reason = classifyP2PRpcFailure(error, signal);
23
+ return { category: "peer", code: authenticationFailureCodes[reason], reason, peerId };
24
+ }
25
+ function pullResultDiagnostic(peerId, status) {
26
+ switch (status) {
27
+ case "completed":
28
+ return { category: "sync", code: "pull-completed", peerId };
29
+ case "cancelled":
30
+ return { category: "sync", code: "pull-cancelled", reason: "operation-cancelled", peerId };
31
+ default:
32
+ return { category: "sync", code: "pull-failed", reason: "replication-error", peerId };
33
+ }
34
+ }
35
+ function pullFailureDiagnostic(peerId, cancelled) {
36
+ if (cancelled) {
37
+ return pullResultDiagnostic(peerId, "cancelled");
38
+ }
39
+ return pullResultDiagnostic(peerId, "failed");
40
+ }
41
+ function notificationFailureDiagnostic(peerId, error) {
42
+ const reason = classifyP2PRpcFailure(error);
43
+ return { category: "sync", code: notificationFailureCodes[reason], reason, peerId };
44
+ }
45
+ export {
46
+ authenticationFailureDiagnostic,
47
+ authenticationResultDiagnostic,
48
+ notificationFailureDiagnostic,
49
+ pullFailureDiagnostic,
50
+ pullResultDiagnostic
51
+ };
@@ -0,0 +1,15 @@
1
+ import type { ProgressInfo } from "../../pouchdb/ReplicatorShim.js";
2
+ import type { P2PReplicationDirectionProgress } from "./TrysteroReplicator.js";
3
+ /** Transient attempt correlation; no replication checkpoint is stored here. */
4
+ export type ProgressAcknowledgement = {
5
+ attempt: number;
6
+ finalProgress?: ProgressInfo;
7
+ };
8
+ export type AcknowledgedProgressAttempt = {
9
+ attempt: number;
10
+ isActive: boolean;
11
+ checkedChanges: number;
12
+ };
13
+ export declare function projectDirectionProgress(info?: ProgressInfo | null): P2PReplicationDirectionProgress;
14
+ /** Return the next attempt snapshot, or reject stale, reordered, or already-ended progress. */
15
+ export declare function projectAcknowledgedAttempt(previous: AcknowledgedProgressAttempt | undefined, info: ProgressInfo | null | undefined, acknowledgement: ProgressAcknowledgement): AcknowledgedProgressAttempt | undefined;
@@ -0,0 +1,37 @@
1
+ function projectDirectionProgress(info) {
2
+ return {
3
+ max: info?.maxSeqInBatch ?? 0,
4
+ current: info?.lastSeq ?? 0,
5
+ isActive: info != null,
6
+ ...info?.statistics ? { statistics: { ...info.statistics } } : {}
7
+ };
8
+ }
9
+ function projectAcknowledgedAttempt(previous, info, acknowledgement) {
10
+ if (!previous) {
11
+ return {
12
+ attempt: acknowledgement.attempt,
13
+ isActive: info != null,
14
+ checkedChanges: info?.statistics?.checkedChanges ?? 0
15
+ };
16
+ }
17
+ if (acknowledgement.attempt < previous.attempt) {
18
+ return void 0;
19
+ }
20
+ if (acknowledgement.attempt === previous.attempt) {
21
+ if (!previous.isActive) {
22
+ return void 0;
23
+ }
24
+ if ((info?.statistics?.checkedChanges ?? previous.checkedChanges) < previous.checkedChanges) {
25
+ return void 0;
26
+ }
27
+ }
28
+ return {
29
+ attempt: acknowledgement.attempt,
30
+ isActive: info != null,
31
+ checkedChanges: info?.statistics?.checkedChanges ?? previous.checkedChanges
32
+ };
33
+ }
34
+ export {
35
+ projectAcknowledgedAttempt,
36
+ projectDirectionProgress
37
+ };
@@ -5,6 +5,7 @@ import type { Confirm } from "../../interfaces/Confirm.js";
5
5
  import type { AsyncActivityRunner } from "../../interfaces/AsyncActivityRunner.js";
6
6
  import type { LiveSyncEventHub } from "../../hub/hub.js";
7
7
  import type { MessageTranslator } from "../../services/base/MessageTranslator.js";
8
+ import type { P2PDiagnosticReporter } from "../../p2p/p2pDiagnostics.js";
8
9
  import type { P2PAutomationCoordinator } from "./P2PAutomationCoordinator.js";
9
10
  export declare const DIRECTION_REQUEST = "request";
10
11
  export type DIRECTION_REQUEST = typeof DIRECTION_REQUEST;
@@ -78,6 +79,8 @@ export interface ReplicatorHost {
78
79
  confirm: Confirm;
79
80
  }
80
81
  export interface ReplicatorHostEnv extends ReplicatorHost {
82
+ /** Observation port fenced by the owning room session; it cannot control transport work. */
83
+ reportDiagnostic?: P2PDiagnosticReporter;
81
84
  /** Event channel owned by the containing Commonlib service composition. */
82
85
  events: LiveSyncEventHub;
83
86
  /** Message translation owned by the containing Commonlib service composition. */
@@ -72,6 +72,16 @@ export declare abstract class APIService<T extends ServiceContext = ServiceConte
72
72
  * @param handler The handler function for the protocol.
73
73
  */
74
74
  abstract registerProtocolHandler(action: string, handler: (params: Record<string, string>) => unknown): void;
75
+ /**
76
+ * Register synchronous cleanup for this host's normal terminal unload.
77
+ * The default uses the context-owned platform-unloaded event once per registration,
78
+ * after lifecycle handlers and local database closure. An unload interrupted before
79
+ * that event does not execute these callbacks.
80
+ * Native hosts may override this to retain their own unload timing. Callbacks are
81
+ * not awaited; resource retirement which must finish before database closure
82
+ * belongs to AppLifecycleService.onUnload instead.
83
+ */
84
+ registerCleanup(callback: () => void): void;
75
85
  /**
76
86
  * Get the basic UI component for showing a confirmation dialog to the user.
77
87
  */
@@ -1,3 +1,4 @@
1
+ import { EVENT_PLATFORM_UNLOADED } from "../../events/coreEvents.js";
1
2
  import { ServiceBase } from "./ServiceBase.js";
2
3
  import { reactiveSource } from "octagonal-wheels/dataobject/reactive";
3
4
  import { _fetch, compatGlobal } from "../../common/coreEnvFunctions.js";
@@ -9,6 +10,18 @@ class APIService extends ServiceBase {
9
10
  showWindowOnRight(type) {
10
11
  return this.showWindow(type);
11
12
  }
13
+ /**
14
+ * Register synchronous cleanup for this host's normal terminal unload.
15
+ * The default uses the context-owned platform-unloaded event once per registration,
16
+ * after lifecycle handlers and local database closure. An unload interrupted before
17
+ * that event does not execute these callbacks.
18
+ * Native hosts may override this to retain their own unload timing. Callbacks are
19
+ * not awaited; resource retirement which must finish before database closure
20
+ * belongs to AppLifecycleService.onUnload instead.
21
+ */
22
+ registerCleanup(callback) {
23
+ this.context.events.onceEvent(EVENT_PLATFORM_UNLOADED, callback);
24
+ }
12
25
  requestCount = reactiveSource(0);
13
26
  responseCount = reactiveSource(0);
14
27
  get isOnline() {
@@ -49,6 +49,8 @@ export interface IAPIService {
49
49
  registerWindow<T>(type: string, factory: (leaf: T) => unknown): void;
50
50
  addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => unknown): HTMLElement;
51
51
  registerProtocolHandler(action: string, handler: (params: Record<string, string>) => unknown): void;
52
+ /** Registers synchronous cleanup at host terminal unload; callbacks are not awaited. */
53
+ registerCleanup(callback: () => void): void;
52
54
  confirm: Confirm;
53
55
  responseCount: ReactiveSource<number>;
54
56
  requestCount: ReactiveSource<number>;
@@ -2,7 +2,7 @@
2
2
  date: 2026-11-03
3
3
  commonlib-version: "0.3.0"
4
4
  self-hosted-livesync-version: "1.0.44"
5
- status: accepted-unreleased
5
+ status: published
6
6
  ---
7
7
 
8
8
  # Architectural Decision Record: Shared Storage Path Resolution
@@ -16,7 +16,9 @@ lifecycle. The [design](../design_docs/storage_path_resolution.md) defines the
16
16
  accepted contract and remaining adoption work.
17
17
 
18
18
  Accepted on 2026-11-03 against the versions recorded above. The implementation
19
- is unreleased. Publication does not by itself enable NFC in WebApp or Bridge.
19
+ is published in [Commonlib 0.3.1](https://www.npmjs.com/package/@vrtmrz/livesync-commonlib/v/0.3.1).
20
+ The original date and reviewed versions retain the accepted baseline.
21
+ Publication does not by itself enable NFC in WebApp or Bridge.
20
22
 
21
23
  ## Context
22
24
 
@@ -2,7 +2,7 @@
2
2
  date: 2026-11-03
3
3
  commonlib-version: "0.3.0"
4
4
  self-hosted-livesync-version: "1.0.44"
5
- status: accepted-unreleased
5
+ status: published
6
6
  ---
7
7
 
8
8
  # Shared storage path resolution
@@ -10,8 +10,8 @@ status: accepted-unreleased
10
10
  This design moves the CLI's existing Unicode-aware path resolution into
11
11
  Commonlib so Node, File System Access, and Bridge can apply the same naming
12
12
  rules. The shared resolver, path types, and Node and File System Access
13
- integrations are implemented as unreleased additions. Installing the package
14
- does not enable NFC in an existing host or migrate its database.
13
+ integrations are published in Commonlib 0.3.1. Installing the package does
14
+ not enable NFC in an existing host or migrate its database.
15
15
 
16
16
  Accepted on 2026-11-03. The [decision record](../adr/2026_11_shared_storage_path_resolution.md)
17
17
  records the ownership and compatibility choices. The [platform guide](../platform-storage.md#unicode-path-resolution)
@@ -258,10 +258,11 @@ of state, including hosts which report them as deletion and creation.
258
258
  ## Current adoption and publication status
259
259
 
260
260
  The shared `/storage` contract, Node and File System Access adapters, and
261
- explicit `DirectFileManipulator` path-version option are unreleased additions
262
- after the published Commonlib `0.3.0` artefact. They require a new immutable
263
- package version; a source checkout with version string `0.3.0` is not the
264
- registry artefact.
261
+ explicit `DirectFileManipulator` path-version option are published in
262
+ [Commonlib 0.3.1](https://www.npmjs.com/package/@vrtmrz/livesync-commonlib/v/0.3.1).
263
+ The original decision date and reviewed versions above retain the accepted
264
+ baseline. These additions are absent from the registry artefact `0.3.0`;
265
+ a source checkout carrying that version label is not the published artefact.
265
266
 
266
267
  The CLI candidate uses the shared Node resolver, and the current Obsidian
267
268
  candidate uses shared namespace and comparison helpers while retaining Vault
@@ -21,8 +21,8 @@ Files elsewhere under `src` are implementation details unless a focused entry re
21
21
 
22
22
  ## Storage path boundary
23
23
 
24
- The unreleased `/storage` entry owns namespace helpers, comparison keys, and
25
- strict component resolution. The [design](design_docs/storage_path_resolution.md)
24
+ The `/storage` entry, published in Commonlib 0.3.1, owns namespace helpers,
25
+ comparison keys, and strict component resolution. The [design](design_docs/storage_path_resolution.md)
26
26
  and [decision record](adr/2026_11_shared_storage_path_resolution.md) separate
27
27
  that contract from host handles, caches, watchers, and migration. See the
28
28
  [platform guide](platform-storage.md#unicode-path-resolution) for composition.
@@ -0,0 +1,69 @@
1
+ ---
2
+ date: 2026-11-04
3
+ commonlib-version: "0.3.1"
4
+ self-hosted-livesync-version: "1.0.44"
5
+ status: published
6
+ published-in: "0.3.2"
7
+ publication-date: 2026-11-05
8
+ ---
9
+
10
+ # Journal receive failures and recovery
11
+
12
+ This document describes receive behaviour published in
13
+ [Commonlib 0.3.2](https://github.com/vrtmrz/livesync-commonlib/releases/tag/0.3.2).
14
+ Hosts choose when to offer the manual skip operation and remain responsible
15
+ for Vault reflection and conflict handling.
16
+
17
+ ## Stop and retry by default
18
+
19
+ A per-Journal decode, record-processing, or write failure stops receipt. The
20
+ failed Journal and all later Journals remain unacknowledged. The next normal
21
+ receive attempt retries the failed Journal before proceeding to later Journals.
22
+ Partial progress does not change this rule.
23
+
24
+ Chunks are written before Metadata, so a Chunk failure prevents that Journal's
25
+ Metadata stage. Metadata retains its received revision ancestry through
26
+ `new_edits: false`, whose empty success result is valid. Successfully written
27
+ records remain in the database when another record fails. There is no rollback.
28
+
29
+ A retry avoids rewriting existing records and delivers accepted Metadata to
30
+ the host again, including revisions saved before an earlier partial failure.
31
+ The host must tolerate repeated delivery. Chunk-arrival events describe only
32
+ successful writes. The `knownIDs` set suppresses unnecessary
33
+ outbound transfer; it does not prove that a receive has completed locally.
34
+
35
+ Warnings identify the failed Journal key and the stage where receipt stopped,
36
+ without logging document contents. A stop request lets current I/O settle and
37
+ closes the receive operation; it is not a Journal failure.
38
+
39
+ ## Skip the stopped Journal and resume
40
+
41
+ When receipt stops on a Journal failure, a host may offer the explicit
42
+ 'Skip stopped Journal and resume' operation. It accepts the exact stopped
43
+ failure snapshot, marks only that Journal received, and resumes receipt of the
44
+ remaining Journals. If another Journal fails, receipt stops at that Journal
45
+ and requires a separate decision.
46
+
47
+ Skipping does not roll back partial writes. It can leave content or revision
48
+ history missing, and receipt of later Journals does not prove that every file
49
+ can be reconstructed or reflected to storage. Each skip acknowledges the
50
+ failed Journal despite its warning.
51
+
52
+ The stopped failure is runtime state, not a persistent skip mode or failure
53
+ queue. After a restart, normal receipt must encounter the failure again before
54
+ the host can offer a skip for it.
55
+
56
+ ## Replay and host boundaries
57
+
58
+ The existing 'Reset journal received history' operation clears `receivedFiles`
59
+ and `knownIDs`, while retaining the local database and `sentFiles`. After the
60
+ failure has been corrected, resetting this history lets receipt replay remote
61
+ Journals other than those sent by this device. Replay requires the affected
62
+ remote objects to remain available and readable; resetting history cannot
63
+ recreate missing remote bytes.
64
+
65
+ A host operation which replaces the local database, such as CLI Fetch, has
66
+ separate reconstruction and completion rules. Repeating Fetch is not the
67
+ same-database retry described above. Journal receipt also remains separate
68
+ from Vault reflection and conflict handling; hosts must preserve their own
69
+ failure and content-protection checks.
@@ -55,19 +55,126 @@ Ordinary consumers use the focused views returned by the service feature. They d
55
55
 
56
56
  The stable view objects close over the same private context, but do not expose that context or one another. They are capability boundaries for consumers, not separately allocated owners. A consumer receives only the views it needs.
57
57
 
58
- | View | Implemented responsibility |
59
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
60
- | `P2PTransportLifecycle` | observe the current connection and accept explicit connect or disconnect intent |
61
- | `P2PConnectionProbeAdmission` | arbitrate a complete Setup signalling check against the current relay binding |
62
- | `P2PPeerDirectory` | supply a current peer advertisement snapshot |
63
- | `P2PPeerAdmission` | apply or revoke temporary and persisted acceptance decisions |
64
- | `P2PTargetedTransfer` | pull, request push, synchronise one peer, or execute the persisted configured-target set without peer-selection dialogue |
65
- | `P2PChangeRelay` | watch or unwatch a peer and enable or disable local-change broadcast |
66
- | `P2PConfigurationExchange` | retrieve configuration from one explicitly selected peer |
67
- | `P2PDiagnostics` | request status and project RTC metrics without exposing the room or peer connection |
58
+ | View | Implemented responsibility |
59
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
60
+ | `P2PTransportLifecycle` | observe the current connection and accept explicit connect or disconnect intent |
61
+ | `P2PConnectionProbeAdmission` | arbitrate a complete Setup signalling check against the current relay binding |
62
+ | `P2PPeerDirectory` | supply a current peer advertisement snapshot |
63
+ | `P2PPeerAdmission` | apply or revoke temporary and persisted acceptance decisions |
64
+ | `P2PTargetedTransfer` | pull, request push, synchronise one peer, or execute the persisted configured-target set without peer-selection dialogue |
65
+ | `P2PChangeRelay` | watch or unwatch a peer and enable or disable local-change broadcast |
66
+ | `P2PConfigurationExchange` | retrieve configuration from one explicitly selected peer |
67
+ | `P2PDiagnostics` | observe diagnostic events and replication progress, request status, and project RTC metrics without exposing transport handles |
68
68
 
69
69
  The optional host UI factory is an explicit compatibility boundary. Commonlib supplies both the deprecated compatibility Replicator and the stable views. Maintained modal transport and status controls use the views; legacy interactive execution and rebuild-specific setup markers may still use the compatibility Replicator. Receiving that façade does not transfer ownership of the room. `UseP2PReplicatorResult.replicator` remains deprecated for the same bounded migration work.
70
70
 
71
+ ## Per-peer replication progress
72
+
73
+ `P2PDiagnostics.getReplicationProgress?()` supplies a read-only snapshot sorted by
74
+ peer ID. `onReplicationProgress?(listener)` observes subsequent snapshots and
75
+ returns a subscription disposer. Hosts should check that these optional methods
76
+ exist when composing an older implementation. The service owns a `P2PLogCollector`
77
+ for these views, and releases its event registrations on platform unload. Peer
78
+ departure removes that peer, and room disconnect or replacement clears the snapshot.
79
+ Repeated advertisements update the peer name and retain its structured progress;
80
+ the legacy map and text keep their existing advertisement reset behaviour.
81
+
82
+ Each direction retains the established `current` and `max` numeric source sequence
83
+ values and `isActive` flag. `P2PLogCollector.p2pReplicationResult` and
84
+ `p2pReplicationLine` keep those sequence units and the existing text format.
85
+ Sequence subtraction does not measure transferred documents.
86
+
87
+ A direction may additionally supply `statistics: { documents, checkedChanges }`:
88
+
89
+ - `documents` counts successfully transferred database document revisions, including
90
+ Metadata and Chunks. It excludes already-held revisions and refresh reads. Two
91
+ missing conflict revisions of one document count as two transfers.
92
+ - `checkedChanges` counts changes-feed entries processed for revision comparison,
93
+ including entries which require no transfer and repeated entries checked again.
94
+
95
+ Both counters start at zero for each finite pull attempt, accumulate across its
96
+ batches, and retain their final values when the direction becomes inactive. The
97
+ sending direction observes the receiver's acknowledgements. An absent
98
+ `statistics` field means the peer has not supplied these counts; it differs from
99
+ an observed zero. A completed all-known batch updates statistics without invoking
100
+ the legacy document-processing callback. That callback still receives the
101
+ refreshed documents only when the batch has missing revisions.
102
+
103
+ The shim's optional `onStatistics` observer also reports accepted writes when a
104
+ later write, refresh, or checkpoint step fails. These counts do not establish a
105
+ committed checkpoint or a completed replication attempt. The observer's sequence
106
+ retains the latest successfully processed batch boundary when a later batch fails
107
+ or is cancelled before writing; the document callback keeps its original batch
108
+ sequence values. Observer failures do not change document processing or checkpoint
109
+ safety.
110
+
111
+ Progress uses the existing `onProgressAcknowledged` RPC. An optional third argument
112
+ carries a transient attempt number, allocated monotonically across room sessions
113
+ within the receiver's module lifetime. Trystero's default peer ID is also generated
114
+ for that module lifetime. The sending observer ignores older attempts, reordered
115
+ batch counts, and updates received after the same attempt's terminal marker. The
116
+ terminal acknowledgement keeps the legacy absent progress argument and adds the
117
+ final progress in its optional metadata, so a terminal marker which overtakes a
118
+ batch still supplies final sequences and counts. Older peers can ignore the extra
119
+ argument and continue using sequence progress. No handshake, retry, heartbeat, or
120
+ persistent replication state is added.
121
+
122
+ ## Latest diagnostic observations
123
+
124
+ The optional `P2PDiagnostics.getDiagnosticSnapshot()` and
125
+ `onDiagnosticEvent(listener)` methods expose the latest `relay`, `peer`, and
126
+ `sync` observations. A missing category is unobserved, rather than a successful
127
+ result or a measured zero. Each immutable event contains a source timestamp in
128
+ milliseconds since the Unix epoch, a fixed code, an optional fixed reason, and an
129
+ optional peer ID. Events do not include device names, settings, room IDs, keys,
130
+ SDP, IP addresses, or arbitrary error text.
131
+
132
+ The stable service owns only one latest event per category. Peer departure and
133
+ room closure retain the latest peer and sync evidence after the peer row has
134
+ disappeared. Room replacement fences the previous session's reporting port before
135
+ retirement, so a late authentication result, notification, or transport callback
136
+ cannot overwrite the current session's evidence. Platform unload detaches the
137
+ store and its subscribers. Older diagnostics implementations may omit both methods.
138
+
139
+ Accepted observations are also published once through the context-owned
140
+ `EVENT_P2P_DIAGNOSTIC` channel exported from `/p2p`. Hosts can format that same
141
+ stream for their summary and diagnostic log. Identical events with the same source
142
+ timestamp are coalesced; repeated advertisements of an already-known peer and
143
+ unchanged watch decisions do not produce another diagnostic transition. No
144
+ server-snapshot, document, or batch progress subscription is used for this stream.
145
+ Diagnostic subscribers and source reporting callbacks cannot change authentication,
146
+ replication outcomes, or room lifecycle control flow. Hosts using the general
147
+ EventHub channel should contain exceptions in their own listener, as with their
148
+ other EventHub listeners.
149
+
150
+ Owned observations include room opening, opening completion or failure, closure,
151
+ join-error callbacks, validated first peer advertisements, peer departure,
152
+ authentication acceptance or refusal, classified RPC failures, finite pull
153
+ outcomes, and watch or change-notification boundaries. Authentication refusal,
154
+ RPC timeout, cancellation, and other RPC errors have distinct fixed codes while
155
+ retaining the existing boolean and replication result contracts. Only an explicit
156
+ `false` authentication response indicates refusal. Missing or non-boolean responses
157
+ use `auth-error` with `authentication-unobserved`, while keeping the existing return
158
+ value and admission policy. An unavailable
159
+ remote broadcast status remains unobserved rather than being reported as an
160
+ explicit false result. Broadcast enablement is observed only after feed creation,
161
+ and an unexpected feed error supplies a fixed failure reason when broadcasting
162
+ stops. No diagnostic operation retries, reconnects, or alters a checkpoint.
163
+
164
+ A room-opened observation means that the owned Trystero room is active. It does
165
+ not establish that a Nostr relay accepted its advertisement. In the reviewed
166
+ `@trystero-p2p/nostr` 0.26.0 API, relay `NOTICE`, rejected `OK`, and `CLOSED`
167
+ messages are written internally to the console without an owned callback. The
168
+ public socket getter is shared across rooms and replaces sockets during
169
+ reconnection. This diagnostic stream therefore uses `relay-details-unobserved`
170
+ and does not intercept console output or WebSocket globals, attach to shared
171
+ sockets, or poll. Relay rejection text remains unavailable through this API.
172
+
173
+ The existing detailed RTC failure subscription is also process-wide and supplies
174
+ no peer or room attribution. Its typed failure reasons remain in the existing
175
+ Diag logging path. They are not presented as a failure of an arbitrary owned room
176
+ or peer in the new stream.
177
+
71
178
  ## Room demands and automation generations
72
179
 
73
180
  `P2PRoomSessionOwner` opens a room while at least one room demand exists. Persistent demands are currently `explicit`, `automatic`, and `rebuild-continuation`. A finite target operation receives an unexported symbol demand for its own lifetime. Removing one demand cannot close a room retained by another persistent or finite demand.
@@ -63,8 +63,8 @@ The File System Access API does not expose portable creation times or permit set
63
63
 
64
64
  ## Unicode path resolution
65
65
 
66
- The following APIs are unreleased additions after Commonlib 0.3.0. Node and
67
- File System Access hosts can use the same NFC comparison and component
66
+ These APIs are published in [Commonlib 0.3.1](https://www.npmjs.com/package/@vrtmrz/livesync-commonlib/v/0.3.1).
67
+ Node and File System Access hosts can use the same NFC comparison and component
68
68
  resolution while retaining the names listed by their filesystem. Hosts still
69
69
  own the selected identity mode and its database migration.
70
70
 
package/docs/releasing.md CHANGED
@@ -67,6 +67,12 @@ For 0.2.3, the documented exceptions are the `HandlerFunction.setHandler` and `L
67
67
 
68
68
  Version 0.3.0 introduces a source compatibility change: the custom client passed to `LiveSyncJournalReplicator.ensureBucketIsCompatible` must provide `hasRemoteJournals(): Promise<boolean>`. The operation distinguishes an empty bucket from Journal files left without an identifying milestone. Keeping it required makes missing client capability a type error. The standard `JournalSyncCore` client already supplies it. This change uses the next minor version under the policy above.
69
69
 
70
+ Version 0.4.0 changes the P2P action wire format by adopting Trystero 0.26.0.
71
+ Communicating peers must use compatible transport versions. Custom `IAPIService`
72
+ implementations must also provide `registerCleanup(callback: () => void): void`;
73
+ subclasses of `APIService` inherit its default terminal-unload implementation.
74
+ These changes use the next minor version under the compatibility policy.
75
+
70
76
  ## Preparing a release
71
77
 
72
78
  Choose the version explicitly. For the 0.2 line, start at 0.2.0. A stable candidate can use its final version on the `next` dist-tag, then be checked by exact source revision and tarball SHA-256 before the same version is promoted to `latest`. No `-next.N` or `-rc.N` version is required; use a separate prerelease version only when it is useful for a distinct candidate. Package-proof versions are local artefacts and cannot be staged.
@@ -109,11 +109,13 @@ the package protocol support alone does not mean those host integrations have
109
109
  shipped.
110
110
 
111
111
  The shared `/storage` resolver, Node and File System Access adapters, and
112
- explicit `DirectFileManipulator` path-version option are later unpublished
113
- additions described in the [shared storage path design](design_docs/storage_path_resolution.md).
114
- They are not in the registry artefact `0.3.0` and require a new immutable
115
- Commonlib package version. A source checkout carrying the same version string
116
- does not make these additions part of the published artefact.
112
+ explicit `DirectFileManipulator` path-version option are published in
113
+ [Commonlib 0.3.1](https://www.npmjs.com/package/@vrtmrz/livesync-commonlib/v/0.3.1),
114
+ as described in the [shared storage path design](design_docs/storage_path_resolution.md).
115
+ They are absent from the registry artefact `0.3.0`; select the new immutable
116
+ version rather than a source checkout carrying an older version label. Shared
117
+ path adoption remains in [LiveSync #1315](https://github.com/vrtmrz/obsidian-livesync/pull/1315)
118
+ and [Bridge #92](https://github.com/vrtmrz/livesync-bridge/pull/92).
117
119
 
118
120
  | Consumer surface | Published baseline | Current candidate status |
119
121
  | --- | --- | --- |
@@ -122,7 +124,7 @@ does not make these additions part of the published artefact.
122
124
  | Bridge | Legacy version `0` remains the default baseline. | The Bridge candidate enables version `1` only by explicit peer opt-in; every peer in a group must use the same path version. Its ordinary case-mismatch check remains separate from the required version check. |
123
125
  | WebApp | Legacy version `0` only. | WebApp remains Legacy-only; an explicit version `1` is rejected at startup. The browser storage primitive alone does not enable application support. |
124
126
  | WebPeer | A vaultless P2P utility. | It is not an NFC filesystem host. |
125
- | `DirectFileManipulator` | The published package rejects a remote declaring the NFC feature. | The unpublished option defaults to version `0`, accepts explicit `0` or `1`, and rejects a populated database in the opposite mode. The caller still owns physical path handling. |
127
+ | `DirectFileManipulator` | Commonlib `0.3.1` supports explicit version `1`; omitted or `0` retains Legacy identity. A populated database in the opposite mode is rejected. | The caller still owns physical path handling and coordinated migration. |
126
128
 
127
129
  `PathService` owns logical ID derivation. In the unreleased LiveSync host
128
130
  candidate, the CLI resolves NFC-equivalent names against directory entries
@@ -172,8 +174,7 @@ checkpoints; the P2P source mark and target checkpoint handle database
172
174
  replacement. These operations do not rewrite IDs or merge histories in place.
173
175
  P2P retains its existing source selection during Fetch.
174
176
 
175
- Published Commonlib direct file access refuses a remote which declares the NFC
176
- feature. The unpublished `DirectFileManipulator` addition exposes explicit
177
+ Commonlib 0.3.1 `DirectFileManipulator` exposes explicit
177
178
  `pathNormalizationVersion` options, defaulting to `0`: missing or `0` retains
178
179
  Legacy and rejects an NFC database; `1` accepts NFC and rejects a populated
179
180
  Legacy database before feature negotiation can change its declaration. A
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrtmrz/livesync-commonlib",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Shared data, storage, and replication primitives for Self-hosted LiveSync clients.",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -1020,7 +1020,7 @@
1020
1020
  "@smithy/middleware-apply-body-checksum": "^4.3.9",
1021
1021
  "@smithy/types": "^4.14.3",
1022
1022
  "@smithy/util-retry": "^4.4.5",
1023
- "@trystero-p2p/nostr": "0.25.3",
1023
+ "@trystero-p2p/nostr": "0.26.0",
1024
1024
  "@vrtmrz/value-store": "0.1.0",
1025
1025
  "diff-match-patch": "^1.0.5",
1026
1026
  "events": "^3.3.0",