@enbox/agent 0.5.13 → 0.5.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.mjs +9 -9
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/sync-engine-level.js +450 -308
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-link-id.js +20 -0
- package/dist/esm/sync-link-id.js.map +1 -0
- package/dist/esm/sync-link-reconciler.js +106 -0
- package/dist/esm/sync-link-reconciler.js.map +1 -0
- package/dist/esm/sync-replication-ledger.js +28 -1
- package/dist/esm/sync-replication-ledger.js.map +1 -1
- package/dist/types/sync-engine-level.d.ts +53 -10
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/dist/types/sync-link-id.d.ts +17 -0
- package/dist/types/sync-link-id.d.ts.map +1 -0
- package/dist/types/sync-link-reconciler.d.ts +57 -0
- package/dist/types/sync-link-reconciler.d.ts.map +1 -0
- package/dist/types/sync-replication-ledger.d.ts +9 -0
- package/dist/types/sync-replication-ledger.d.ts.map +1 -1
- package/dist/types/types/sync.d.ts +14 -5
- package/dist/types/types/sync.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/sync-engine-level.ts +478 -332
- package/src/sync-link-id.ts +24 -0
- package/src/sync-link-reconciler.ts +155 -0
- package/src/sync-replication-ledger.ts +27 -1
- package/src/types/sync.ts +9 -3
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Separator used in compound runtime/legacy cursor keys. */
|
|
2
|
+
export const LINK_ID_SEPARATOR = '^';
|
|
3
|
+
|
|
4
|
+
/** Opaque runtime identifier for a replication link. */
|
|
5
|
+
export type LinkId = string;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build the runtime identifier for a replication link.
|
|
9
|
+
*
|
|
10
|
+
* Runtime identity is `(tenantDid, remoteEndpoint, scopeId)`.
|
|
11
|
+
*/
|
|
12
|
+
export function buildLinkId(tenantDid: string, remoteEndpoint: string, scopeId: string): LinkId {
|
|
13
|
+
return `${tenantDid}${LINK_ID_SEPARATOR}${remoteEndpoint}${LINK_ID_SEPARATOR}${scopeId}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the legacy cursor key used by the deprecated `syncCursors` sublevel.
|
|
18
|
+
*
|
|
19
|
+
* This remains only for one-time migration of pre-Phase-1f data.
|
|
20
|
+
*/
|
|
21
|
+
export function buildLegacyCursorKey(tenantDid: string, remoteEndpoint: string, protocol?: string): string {
|
|
22
|
+
const base = `${tenantDid}${LINK_ID_SEPARATOR}${remoteEndpoint}`;
|
|
23
|
+
return protocol ? `${base}${LINK_ID_SEPARATOR}${protocol}` : base;
|
|
24
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { GenericMessage, MessagesSyncDiffEntry } from '@enbox/dwn-sdk-js';
|
|
2
|
+
|
|
3
|
+
import type { PushResult } from './types/sync.js';
|
|
4
|
+
|
|
5
|
+
export type ReconcileDirection = 'pull' | 'push';
|
|
6
|
+
|
|
7
|
+
export type ReconcileTarget = {
|
|
8
|
+
did: string;
|
|
9
|
+
dwnUrl: string;
|
|
10
|
+
delegateDid?: string;
|
|
11
|
+
protocol?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ReconcileOutcome = {
|
|
15
|
+
aborted?: boolean;
|
|
16
|
+
changed: boolean;
|
|
17
|
+
didPull: boolean;
|
|
18
|
+
didPush: boolean;
|
|
19
|
+
localRoot?: string;
|
|
20
|
+
remoteRoot?: string;
|
|
21
|
+
postLocalRoot?: string;
|
|
22
|
+
postRemoteRoot?: string;
|
|
23
|
+
converged?: boolean;
|
|
24
|
+
pushResult?: PushResult;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ReconcileDeps = {
|
|
28
|
+
getLocalRoot: (did: string, delegateDid?: string, protocol?: string) => Promise<string>;
|
|
29
|
+
getRemoteRoot: (did: string, dwnUrl: string, delegateDid?: string, protocol?: string) => Promise<string>;
|
|
30
|
+
diffWithRemote: (target: ReconcileTarget) => Promise<{ onlyRemote: MessagesSyncDiffEntry[]; onlyLocal: string[] }>;
|
|
31
|
+
pullMessages: (params: {
|
|
32
|
+
did: string;
|
|
33
|
+
dwnUrl: string;
|
|
34
|
+
delegateDid?: string;
|
|
35
|
+
protocol?: string;
|
|
36
|
+
messageCids: string[];
|
|
37
|
+
prefetched: (MessagesSyncDiffEntry & { message: GenericMessage })[];
|
|
38
|
+
}) => Promise<void>;
|
|
39
|
+
pushMessages: (params: {
|
|
40
|
+
did: string;
|
|
41
|
+
dwnUrl: string;
|
|
42
|
+
delegateDid?: string;
|
|
43
|
+
protocol?: string;
|
|
44
|
+
messageCids: string[];
|
|
45
|
+
}) => Promise<PushResult>;
|
|
46
|
+
shouldContinue?: () => boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function partitionRemoteEntries(entries: MessagesSyncDiffEntry[]): {
|
|
50
|
+
prefetched: (MessagesSyncDiffEntry & { message: GenericMessage })[];
|
|
51
|
+
needsFetchCids: string[];
|
|
52
|
+
} {
|
|
53
|
+
const prefetched: (MessagesSyncDiffEntry & { message: GenericMessage })[] = [];
|
|
54
|
+
const needsFetchCids: string[] = [];
|
|
55
|
+
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (!entry.message) {
|
|
58
|
+
needsFetchCids.push(entry.messageCid);
|
|
59
|
+
} else if (
|
|
60
|
+
entry.message.descriptor.interface === 'Records' &&
|
|
61
|
+
entry.message.descriptor.method === 'Write' &&
|
|
62
|
+
(entry.message.descriptor as any).dataCid &&
|
|
63
|
+
!entry.encodedData
|
|
64
|
+
) {
|
|
65
|
+
needsFetchCids.push(entry.messageCid);
|
|
66
|
+
} else {
|
|
67
|
+
prefetched.push(entry as MessagesSyncDiffEntry & { message: GenericMessage });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { prefetched, needsFetchCids };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class SyncLinkReconciler {
|
|
75
|
+
private readonly _deps: ReconcileDeps;
|
|
76
|
+
|
|
77
|
+
constructor(deps: ReconcileDeps) {
|
|
78
|
+
this._deps = deps;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public async reconcile(target: ReconcileTarget, options?: {
|
|
82
|
+
direction?: ReconcileDirection;
|
|
83
|
+
verifyConvergence?: boolean;
|
|
84
|
+
}): Promise<ReconcileOutcome> {
|
|
85
|
+
const { did, dwnUrl, delegateDid, protocol } = target;
|
|
86
|
+
const direction = options?.direction;
|
|
87
|
+
const verifyConvergence = options?.verifyConvergence ?? false;
|
|
88
|
+
const shouldContinue = this._deps.shouldContinue;
|
|
89
|
+
|
|
90
|
+
const localRoot = await this._deps.getLocalRoot(did, delegateDid, protocol);
|
|
91
|
+
if (shouldContinue && !shouldContinue()) { return { aborted: true, changed: false, didPull: false, didPush: false }; }
|
|
92
|
+
|
|
93
|
+
const remoteRoot = await this._deps.getRemoteRoot(did, dwnUrl, delegateDid, protocol);
|
|
94
|
+
if (shouldContinue && !shouldContinue()) { return { aborted: true, changed: false, didPull: false, didPush: false }; }
|
|
95
|
+
|
|
96
|
+
let didPull = false;
|
|
97
|
+
let didPush = false;
|
|
98
|
+
let pushResult: PushResult | undefined;
|
|
99
|
+
|
|
100
|
+
if (localRoot !== remoteRoot) {
|
|
101
|
+
const diff = await this._deps.diffWithRemote(target);
|
|
102
|
+
if (shouldContinue && !shouldContinue()) { return { aborted: true, changed: true, didPull: false, didPush: false, localRoot, remoteRoot }; }
|
|
103
|
+
|
|
104
|
+
if ((!direction || direction === 'pull') && diff.onlyRemote.length > 0) {
|
|
105
|
+
const { prefetched, needsFetchCids } = partitionRemoteEntries(diff.onlyRemote);
|
|
106
|
+
await this._deps.pullMessages({ did, dwnUrl, delegateDid, protocol, messageCids: needsFetchCids, prefetched });
|
|
107
|
+
if (shouldContinue && !shouldContinue()) { return { aborted: true, changed: true, didPull: true, didPush: false, localRoot, remoteRoot }; }
|
|
108
|
+
didPull = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if ((!direction || direction === 'push') && diff.onlyLocal.length > 0) {
|
|
112
|
+
pushResult = await this._deps.pushMessages({
|
|
113
|
+
did, dwnUrl, delegateDid, protocol, messageCids: diff.onlyLocal,
|
|
114
|
+
});
|
|
115
|
+
if (shouldContinue && !shouldContinue()) {
|
|
116
|
+
return { aborted: true, changed: true, didPull, didPush: true, localRoot, remoteRoot, pushResult };
|
|
117
|
+
}
|
|
118
|
+
didPush = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!verifyConvergence) {
|
|
123
|
+
return {
|
|
124
|
+
changed: localRoot !== remoteRoot,
|
|
125
|
+
didPull,
|
|
126
|
+
didPush,
|
|
127
|
+
localRoot,
|
|
128
|
+
remoteRoot,
|
|
129
|
+
pushResult,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const postLocalRoot = await this._deps.getLocalRoot(did, delegateDid, protocol);
|
|
134
|
+
if (shouldContinue && !shouldContinue()) {
|
|
135
|
+
return { aborted: true, changed: localRoot !== remoteRoot, didPull, didPush, localRoot, remoteRoot, pushResult };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const postRemoteRoot = await this._deps.getRemoteRoot(did, dwnUrl, delegateDid, protocol);
|
|
139
|
+
if (shouldContinue && !shouldContinue()) {
|
|
140
|
+
return { aborted: true, changed: localRoot !== remoteRoot, didPull, didPush, localRoot, remoteRoot, pushResult };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
changed : localRoot !== remoteRoot,
|
|
145
|
+
didPull,
|
|
146
|
+
didPush,
|
|
147
|
+
localRoot,
|
|
148
|
+
remoteRoot,
|
|
149
|
+
postLocalRoot,
|
|
150
|
+
postRemoteRoot,
|
|
151
|
+
converged : postLocalRoot === postRemoteRoot,
|
|
152
|
+
pushResult,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -62,6 +62,7 @@ export class ReplicationLedger {
|
|
|
62
62
|
try {
|
|
63
63
|
const raw = await this.sublevel.get(key);
|
|
64
64
|
const link = JSON.parse(raw) as ReplicationLinkState;
|
|
65
|
+
delete (link as any).push; // strip legacy push field from old persisted links
|
|
65
66
|
// connectivity is runtime state — always reset to 'unknown' on load
|
|
66
67
|
// so stale 'online' from a previous session doesn't give false positives.
|
|
67
68
|
link.connectivity = 'unknown';
|
|
@@ -82,7 +83,7 @@ export class ReplicationLedger {
|
|
|
82
83
|
status : 'initializing',
|
|
83
84
|
connectivity : 'unknown',
|
|
84
85
|
pull : {},
|
|
85
|
-
|
|
86
|
+
needsReconcile : false,
|
|
86
87
|
delegateDid : params.delegateDid,
|
|
87
88
|
protocol : params.protocol,
|
|
88
89
|
};
|
|
@@ -193,5 +194,30 @@ export class ReplicationLedger {
|
|
|
193
194
|
checkpoint.contiguousAppliedToken = token;
|
|
194
195
|
checkpoint.receivedToken = token;
|
|
195
196
|
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Reconciliation helpers
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Mark a link as needing SMT reconciliation and persist.
|
|
204
|
+
* Idempotent — no-op if already set.
|
|
205
|
+
*/
|
|
206
|
+
public async markNeedsReconcile(link: ReplicationLinkState, _reason?: string): Promise<void> {
|
|
207
|
+
if (!link.needsReconcile) {
|
|
208
|
+
link.needsReconcile = true;
|
|
209
|
+
await this.saveLink(link);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Clear the reconciliation flag after successful SMT reconciliation.
|
|
215
|
+
*/
|
|
216
|
+
public async clearNeedsReconcile(link: ReplicationLinkState): Promise<void> {
|
|
217
|
+
if (link.needsReconcile) {
|
|
218
|
+
link.needsReconcile = false;
|
|
219
|
+
await this.saveLink(link);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
196
222
|
}
|
|
197
223
|
|
package/src/types/sync.ts
CHANGED
|
@@ -161,8 +161,13 @@ export type ReplicationLinkState = {
|
|
|
161
161
|
/** Pull-direction replication checkpoint (remote → local). */
|
|
162
162
|
pull: DirectionCheckpoint;
|
|
163
163
|
|
|
164
|
-
/**
|
|
165
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Whether this link needs SMT reconciliation. Set when push fails after
|
|
166
|
+
* retry exhaustion, when the link reconnects after an outage, or when
|
|
167
|
+
* the remote epoch changes. Cleared after successful reconciliation.
|
|
168
|
+
* Persisted so recovery survives app/browser restart.
|
|
169
|
+
*/
|
|
170
|
+
needsReconcile?: boolean;
|
|
166
171
|
|
|
167
172
|
/** Per-link connectivity state. Used to compute the aggregate engine-level state. */
|
|
168
173
|
connectivity: SyncConnectivityState;
|
|
@@ -242,7 +247,8 @@ export type SyncEvent =
|
|
|
242
247
|
| { type: 'link:status-change'; tenantDid: string; remoteEndpoint: string; protocol?: string; from: LinkStatus; to: LinkStatus }
|
|
243
248
|
| { type: 'link:connectivity-change'; tenantDid: string; remoteEndpoint: string; protocol?: string; from: SyncConnectivityState; to: SyncConnectivityState }
|
|
244
249
|
| { type: 'checkpoint:pull-advance'; tenantDid: string; remoteEndpoint: string; protocol?: string; position: string; messageCid: string }
|
|
245
|
-
| { type: '
|
|
250
|
+
| { type: 'reconcile:needed'; tenantDid: string; remoteEndpoint: string; protocol?: string; reason: string }
|
|
251
|
+
| { type: 'reconcile:completed'; tenantDid: string; remoteEndpoint: string; protocol?: string }
|
|
246
252
|
| { type: 'repair:started'; tenantDid: string; remoteEndpoint: string; protocol?: string; attempt: number }
|
|
247
253
|
| { type: 'repair:completed'; tenantDid: string; remoteEndpoint: string; protocol?: string }
|
|
248
254
|
| { type: 'repair:failed'; tenantDid: string; remoteEndpoint: string; protocol?: string; attempt: number; error: string }
|