@abraca/dabra 2.8.0 → 2.10.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/dist/abracadabra-provider.cjs +138 -10
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +138 -10
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +48 -4
- package/package.json +2 -2
- package/src/AbracadabraWS.ts +72 -2
- package/src/BackgroundSyncManager.ts +165 -14
- package/src/BackgroundSyncPersistence.ts +35 -2
- package/src/index.ts +4 -1
package/dist/index.d.ts
CHANGED
|
@@ -2212,7 +2212,19 @@ declare class AbracadabraWS extends EventEmitter {
|
|
|
2212
2212
|
resolve: (value?: any) => void;
|
|
2213
2213
|
reject: (reason?: any) => void;
|
|
2214
2214
|
} | null;
|
|
2215
|
+
private onlineListener;
|
|
2216
|
+
private offlineListener;
|
|
2215
2217
|
constructor(configuration: AbracadabraWSConfiguration);
|
|
2218
|
+
/**
|
|
2219
|
+
* Whether the device currently believes it has network connectivity.
|
|
2220
|
+
*
|
|
2221
|
+
* Treats "unknown" as online: in Node and other non-browser environments
|
|
2222
|
+
* `navigator` (or `navigator.onLine`) is absent, and we must not gate
|
|
2223
|
+
* reconnection there — only the browser exposes a trustworthy signal.
|
|
2224
|
+
*/
|
|
2225
|
+
get isOnline(): boolean;
|
|
2226
|
+
handleOnline(): void;
|
|
2227
|
+
handleOffline(): void;
|
|
2216
2228
|
receivedOnOpenPayload?: Event | undefined;
|
|
2217
2229
|
onOpen(event: Event): Promise<void>;
|
|
2218
2230
|
attach(provider: AbracadabraBaseProvider): void;
|
|
@@ -3132,14 +3144,39 @@ declare function attachUpdatedAtObserver(treeMap: Y.Map<any>, childDocId: string
|
|
|
3132
3144
|
*
|
|
3133
3145
|
* Falls back to a no-op when IndexedDB is unavailable (SSR / Node.js).
|
|
3134
3146
|
*/
|
|
3147
|
+
/**
|
|
3148
|
+
* Background-sync lifecycle status.
|
|
3149
|
+
*
|
|
3150
|
+
* - `pending` — never synced; queued for first attempt.
|
|
3151
|
+
* - `syncing` — attempt in progress (transient, not persisted as terminal).
|
|
3152
|
+
* - `synced` — last attempt succeeded; `lastSynced` is set.
|
|
3153
|
+
* - `retrying` — a *transient* failure (timeout, network) that has NOT yet
|
|
3154
|
+
* crossed the failure threshold. Will be retried silently;
|
|
3155
|
+
* the UI should treat this calmly and NOT count it as an error.
|
|
3156
|
+
* - `error` — a transient failure that has recurred past the threshold
|
|
3157
|
+
* (`consecutiveFailures >= FAILURE_THRESHOLD`). Genuinely
|
|
3158
|
+
* needs attention; this is what the UI surfaces as red.
|
|
3159
|
+
* - `unavailable` — a *permanent* failure (doc deleted/forbidden/invalid id —
|
|
3160
|
+
* 404/403/410/422). Not retried, and not surfaced as an error.
|
|
3161
|
+
* - `skipped` — E2E doc we can't sync (no keystore). Benign.
|
|
3162
|
+
*/
|
|
3163
|
+
type DocSyncStatus = "pending" | "syncing" | "synced" | "retrying" | "error" | "unavailable" | "skipped";
|
|
3135
3164
|
interface DocSyncState {
|
|
3136
3165
|
docId: string;
|
|
3137
3166
|
/** Current lifecycle status of this document's background sync. */
|
|
3138
|
-
status:
|
|
3167
|
+
status: DocSyncStatus;
|
|
3139
3168
|
/** Unix ms of the last successful sync, or null if never synced. */
|
|
3140
3169
|
lastSynced: number | null;
|
|
3141
|
-
/**
|
|
3170
|
+
/**
|
|
3171
|
+
* Human-readable message for the most recent failure. Populated for
|
|
3172
|
+
* `retrying`, `error`, and `unavailable` — cleared on a successful sync.
|
|
3173
|
+
*/
|
|
3142
3174
|
error?: string;
|
|
3175
|
+
/**
|
|
3176
|
+
* Count of consecutive failed attempts. Reset to 0 on success. Drives the
|
|
3177
|
+
* `retrying` → `error` escalation so a single transient blip never nags.
|
|
3178
|
+
*/
|
|
3179
|
+
consecutiveFailures?: number;
|
|
3143
3180
|
/** Whether the document uses E2E encryption. */
|
|
3144
3181
|
isE2E: boolean;
|
|
3145
3182
|
}
|
|
@@ -3159,7 +3196,7 @@ declare class BackgroundSyncPersistence {
|
|
|
3159
3196
|
interface BackgroundSyncManagerOptions {
|
|
3160
3197
|
/** Max parallel WS connections for background sync. Default: 2. */
|
|
3161
3198
|
concurrency?: number;
|
|
3162
|
-
/** Timeout (ms) waiting for a provider to sync. Default:
|
|
3199
|
+
/** Timeout (ms) waiting for a provider to sync. Default: 30 000. */
|
|
3163
3200
|
syncTimeout?: number;
|
|
3164
3201
|
/** Pre-cache file blobs after syncing a doc. Default: true. */
|
|
3165
3202
|
prefetchFiles?: boolean;
|
|
@@ -3180,6 +3217,8 @@ declare class BackgroundSyncManager extends EventEmitter {
|
|
|
3180
3217
|
private readonly _warnedInvalidIds;
|
|
3181
3218
|
private _destroyed;
|
|
3182
3219
|
private _initPromise;
|
|
3220
|
+
/** One-shot timer for the post-syncAll straggler retry pass. */
|
|
3221
|
+
private _stragglerTimer;
|
|
3183
3222
|
constructor(rootProvider: AbracadabraProvider, client: AbracadabraClient, fileBlobStore?: FileBlobStore | null, opts?: BackgroundSyncManagerOptions);
|
|
3184
3223
|
/**
|
|
3185
3224
|
* Load persisted sync states from IndexedDB into the in-memory map.
|
|
@@ -3189,6 +3228,11 @@ declare class BackgroundSyncManager extends EventEmitter {
|
|
|
3189
3228
|
private _loadPersistedStates;
|
|
3190
3229
|
/** Sync all documents in the root tree. */
|
|
3191
3230
|
syncAll(): Promise<void>;
|
|
3231
|
+
/**
|
|
3232
|
+
* Schedule a single delayed retry of the given docs. Replaces any pending
|
|
3233
|
+
* straggler timer so repeated `syncAll()` calls don't stack timers.
|
|
3234
|
+
*/
|
|
3235
|
+
private _scheduleStragglerRetry;
|
|
3192
3236
|
/** Sync a single document by ID. */
|
|
3193
3237
|
syncDoc(docId: string): Promise<DocSyncState>;
|
|
3194
3238
|
/** Return a snapshot of all known sync states. */
|
|
@@ -5305,4 +5349,4 @@ declare function patchEntry(treeMap: Y.Map<unknown>, id: string, patch: Record<s
|
|
|
5305
5349
|
allowLabelClear?: boolean;
|
|
5306
5350
|
}): void;
|
|
5307
5351
|
//#endregion
|
|
5308
|
-
export { AbracadabraBaseProvider, AbracadabraBaseProviderConfiguration, AbracadabraClient, AbracadabraClientConfig, AbracadabraOutgoingMessageArguments, AbracadabraProvider, AbracadabraProviderConfiguration, AbracadabraWS, AbracadabraWSConfiguration, AbracadabraWebRTC, type AbracadabraWebRTCConfiguration, AbracadabraWebSocketConn, AdminConfigField, AdminConfigOriginKind, AuditLogEntry, AuditQueryOpts, AuditVerifyResult, AuthFailureContext, AuthFailureReason, AuthMessageType, AuthorizedScope, AwarenessError, BackgroundSyncManager, type BackgroundSyncManagerOptions, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ChannelKeyResolver, type ChatChannel, ChatClient, type ChatClientTransport, type MarkReadInput as ChatMarkReadInput, type ChatMessage, type ChatReadCursor, type ChatReadReceipt, type ChatTypingEvent, type ChildrenPage, CloseEvent, CompleteAbracadabraBaseProviderConfiguration, CompleteAbracadabraWSConfiguration, CompleteHocuspocusProviderConfiguration, CompleteHocuspocusProviderWebsocketConfiguration, ConnectionTimeout, Constructable, ConstructableOutgoingMessage, ContentManager, type CreateNotificationInput, CryptoIdentity, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, type DeleteMessageInput, DevicePairingChannel, type DevicePairingConfig, DeviceRegistrationService, type DeviceServerStatus, type DeviceSessionRecord, type DeviceSessionStorage, type DeviceTier, type DocEncryptionInfo, DocKeyManager, DocSearchHit, type DocSyncState, type DocumentBlock, DocumentCache, type DocumentCacheOptions, type DocumentContent, DocumentManager, type DocumentManagerConfig, DocumentMeta, type DocumentMetaInfo, type DocumentMetaWire, E2EAbracadabraProvider, E2EEChannel, type E2EEIdentity, E2EOfflineStore, type EditMessageInput, EffectivePermissionEntry, EffectivePermissionsResponse, EffectiveRole, EncryptedChatClient, EncryptedYMap, EncryptedYText, EnvSnapshotExtension, EnvSnapshotItem, EnvSnapshotResponse, type FetchInboxInput, type FetchNotificationsInput, FileBlobStore, FileTransferChannel, FileTransferHandle, type FileTransferMeta, type FileTransferStatus, type FoldedMessage, Forbidden, GEO_TYPE_META_SCHEMAS, type GetChatHistoryInput, HealthStatus, HocusPocusWebSocket, HocuspocusProvider, HocuspocusProviderConfiguration, HocuspocusProviderWebsocket, HocuspocusProviderWebsocketConfiguration, HocuspocusWebSocket, type IdentityDeviceEntry, type IdentityDocConfiguration, IdentityDocProvider, type IdentityProfile, type IdentityServerEntry, type IdentitySpaceEntry, type InboxEntry, type MarkReadInput$1 as InboxMarkReadInput, InviteRow, KEY_EXCHANGE_CHANNEL, Kind, LocalStorageDeviceSessionStorage, ManualSignaling, type ManualSignalingBlob, type MessageRecord, MessageTooBig, MessageType, MetaFieldType, MetaManager, MetaValidationError, type NotificationReadUpdate, type NotificationRecord, NotificationsClient, OfflineStore, OutgoingMessageArguments, OutgoingMessageInterface, PAGE_TYPES, PageMeta, PageTypeInfo, PageTypeMetaField, type PairingRequest, type PairingResult, PeerConnection, type PeerInfo, type PeerState, PendingSubdoc, PermissionEntry, PublicKeyInfo, QUERY_PREFIX, QueryClient, QueryError, type QueryFrame, type QueryKind, type QuerySpec, type QuerySubscriptionHandle, type QuerySubscriptionHandlers, type QueryTransport, RPC_PREFIX, ReadyzStatus, ResetConnection, type RpcCallHandle, type RpcCallOptions, RpcClient, RpcError, type RpcErrorCode, type RpcErrorPayload, type RpcFrame, type RpcHandler, type RpcHandlerContext, type RpcKind, type RpcTarget, type RpcTransport, SERVER_ROOT_ID, type SchemaDocTypeName, type SchemaMetaOf, type SchemaRegistryLike, type SchemaValidatorLike, SearchIndex, SearchResult, type SendChatMessageInput, type SendMessageInput, type SendMessageResult, ServerInfo, type SignalingIncoming, type SignalingOutgoing, SignalingSocket, SnapshotCreateResult, SnapshotData, SnapshotFileEntry, SnapshotForkResult, SnapshotMeta, SnapshotRestoreResult, StatesArray, SubdocMessage, SubdocRegisteredEvent, TYPE_ALIASES, TokenManager, type TokenManagerOptions, TreeEntry, TreeManager, TreeNode, TreeSearchResult, TypedDocTypeMismatchError, type TypedDocsClient, type TypedTreeEntry, Unauthorized, UploadInfo, UploadMeta, UploadQueueEntry, UploadQueueStatus, UserMetaField, UserProfile, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, buildBlockquoteElement, buildBlocksFromMarkdown, buildBulletListElement, buildCodeBlockElement, buildHeadingElement, buildHorizontalRuleElement, buildOrderedListElement, buildParagraphElement, buildTaskListElement, decryptChatContent, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptChatContent, encryptField, filenameToLabel, foldRecords, generateMnemonic, isEncryptedContent, isPlaceholderLabel, makeEncryptedYMap, makeEncryptedYText, makeEntryMap, mnemonicToEd25519Seed, mnemonicToKeyPair, normalizeRootId, onAuthenticatedParameters, onAuthenticationFailedParameters, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onCompactedParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onServerErrorParameters, onStatelessParameters, onStatusParameters, onSubdocLoadedParameters, onSubdocRegisteredParameters, onSyncedParameters, onUnsyncedChangesParameters, parseFrontmatter, patchEntry, populateYDocFromMarkdown, readAuthMessage, readBlocksFromFragment, recordFromYAny, resolvePageType, toPlain, unwrapSeed, validateMnemonic, waitForSync, withTimeout, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest, yjsToMarkdown };
|
|
5352
|
+
export { AbracadabraBaseProvider, AbracadabraBaseProviderConfiguration, AbracadabraClient, AbracadabraClientConfig, AbracadabraOutgoingMessageArguments, AbracadabraProvider, AbracadabraProviderConfiguration, AbracadabraWS, AbracadabraWSConfiguration, AbracadabraWebRTC, type AbracadabraWebRTCConfiguration, AbracadabraWebSocketConn, AdminConfigField, AdminConfigOriginKind, AuditLogEntry, AuditQueryOpts, AuditVerifyResult, AuthFailureContext, AuthFailureReason, AuthMessageType, AuthorizedScope, AwarenessError, BackgroundSyncManager, type BackgroundSyncManagerOptions, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ChannelKeyResolver, type ChatChannel, ChatClient, type ChatClientTransport, type MarkReadInput as ChatMarkReadInput, type ChatMessage, type ChatReadCursor, type ChatReadReceipt, type ChatTypingEvent, type ChildrenPage, CloseEvent, CompleteAbracadabraBaseProviderConfiguration, CompleteAbracadabraWSConfiguration, CompleteHocuspocusProviderConfiguration, CompleteHocuspocusProviderWebsocketConfiguration, ConnectionTimeout, Constructable, ConstructableOutgoingMessage, ContentManager, type CreateNotificationInput, CryptoIdentity, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, type DeleteMessageInput, DevicePairingChannel, type DevicePairingConfig, DeviceRegistrationService, type DeviceServerStatus, type DeviceSessionRecord, type DeviceSessionStorage, type DeviceTier, type DocEncryptionInfo, DocKeyManager, DocSearchHit, type DocSyncState, type DocSyncStatus, type DocumentBlock, DocumentCache, type DocumentCacheOptions, type DocumentContent, DocumentManager, type DocumentManagerConfig, DocumentMeta, type DocumentMetaInfo, type DocumentMetaWire, E2EAbracadabraProvider, E2EEChannel, type E2EEIdentity, E2EOfflineStore, type EditMessageInput, EffectivePermissionEntry, EffectivePermissionsResponse, EffectiveRole, EncryptedChatClient, EncryptedYMap, EncryptedYText, EnvSnapshotExtension, EnvSnapshotItem, EnvSnapshotResponse, type FetchInboxInput, type FetchNotificationsInput, FileBlobStore, FileTransferChannel, FileTransferHandle, type FileTransferMeta, type FileTransferStatus, type FoldedMessage, Forbidden, GEO_TYPE_META_SCHEMAS, type GetChatHistoryInput, HealthStatus, HocusPocusWebSocket, HocuspocusProvider, HocuspocusProviderConfiguration, HocuspocusProviderWebsocket, HocuspocusProviderWebsocketConfiguration, HocuspocusWebSocket, type IdentityDeviceEntry, type IdentityDocConfiguration, IdentityDocProvider, type IdentityProfile, type IdentityServerEntry, type IdentitySpaceEntry, type InboxEntry, type MarkReadInput$1 as InboxMarkReadInput, InviteRow, KEY_EXCHANGE_CHANNEL, Kind, LocalStorageDeviceSessionStorage, ManualSignaling, type ManualSignalingBlob, type MessageRecord, MessageTooBig, MessageType, MetaFieldType, MetaManager, MetaValidationError, type NotificationReadUpdate, type NotificationRecord, NotificationsClient, OfflineStore, OutgoingMessageArguments, OutgoingMessageInterface, PAGE_TYPES, PageMeta, PageTypeInfo, PageTypeMetaField, type PairingRequest, type PairingResult, PeerConnection, type PeerInfo, type PeerState, PendingSubdoc, PermissionEntry, PublicKeyInfo, QUERY_PREFIX, QueryClient, QueryError, type QueryFrame, type QueryKind, type QuerySpec, type QuerySubscriptionHandle, type QuerySubscriptionHandlers, type QueryTransport, RPC_PREFIX, ReadyzStatus, ResetConnection, type RpcCallHandle, type RpcCallOptions, RpcClient, RpcError, type RpcErrorCode, type RpcErrorPayload, type RpcFrame, type RpcHandler, type RpcHandlerContext, type RpcKind, type RpcTarget, type RpcTransport, SERVER_ROOT_ID, type SchemaDocTypeName, type SchemaMetaOf, type SchemaRegistryLike, type SchemaValidatorLike, SearchIndex, SearchResult, type SendChatMessageInput, type SendMessageInput, type SendMessageResult, ServerInfo, type SignalingIncoming, type SignalingOutgoing, SignalingSocket, SnapshotCreateResult, SnapshotData, SnapshotFileEntry, SnapshotForkResult, SnapshotMeta, SnapshotRestoreResult, StatesArray, SubdocMessage, SubdocRegisteredEvent, TYPE_ALIASES, TokenManager, type TokenManagerOptions, TreeEntry, TreeManager, TreeNode, TreeSearchResult, TypedDocTypeMismatchError, type TypedDocsClient, type TypedTreeEntry, Unauthorized, UploadInfo, UploadMeta, UploadQueueEntry, UploadQueueStatus, UserMetaField, UserProfile, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, buildBlockquoteElement, buildBlocksFromMarkdown, buildBulletListElement, buildCodeBlockElement, buildHeadingElement, buildHorizontalRuleElement, buildOrderedListElement, buildParagraphElement, buildTaskListElement, decryptChatContent, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptChatContent, encryptField, filenameToLabel, foldRecords, generateMnemonic, isEncryptedContent, isPlaceholderLabel, makeEncryptedYMap, makeEncryptedYText, makeEntryMap, mnemonicToEd25519Seed, mnemonicToKeyPair, normalizeRootId, onAuthenticatedParameters, onAuthenticationFailedParameters, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onCompactedParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onServerErrorParameters, onStatelessParameters, onStatusParameters, onSubdocLoadedParameters, onSubdocRegisteredParameters, onSyncedParameters, onUnsyncedChangesParameters, parseFrontmatter, patchEntry, populateYDocFromMarkdown, readAuthMessage, readBlocksFromFragment, recordFromYAny, resolvePageType, toPlain, unwrapSeed, validateMnemonic, waitForSync, withTimeout, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest, yjsToMarkdown };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abraca/dabra",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "abracadabra provider",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abracadabra",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"yjs": "^13.6.8"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@abraca/schema": "2.
|
|
44
|
+
"@abraca/schema": "2.10.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"test": "node --no-warnings --conditions=source --experimental-transform-types --test 'tests/*.test.ts'"
|
package/src/AbracadabraWS.ts
CHANGED
|
@@ -182,6 +182,12 @@ export class AbracadabraWS extends EventEmitter {
|
|
|
182
182
|
reject: (reason?: any) => void;
|
|
183
183
|
} | null = null;
|
|
184
184
|
|
|
185
|
+
// Bound `online`/`offline` listeners, retained so we can detach them in
|
|
186
|
+
// destroy(). Only registered in browser-like environments (see constructor).
|
|
187
|
+
private onlineListener: (() => void) | null = null;
|
|
188
|
+
|
|
189
|
+
private offlineListener: (() => void) | null = null;
|
|
190
|
+
|
|
185
191
|
constructor(configuration: AbracadabraWSConfiguration) {
|
|
186
192
|
super();
|
|
187
193
|
this.setConfiguration(configuration);
|
|
@@ -210,11 +216,57 @@ export class AbracadabraWS extends EventEmitter {
|
|
|
210
216
|
this.configuration.messageReconnectTimeout / 10,
|
|
211
217
|
);
|
|
212
218
|
|
|
219
|
+
// Offline-first reconnect gating. When the device reports it is offline we
|
|
220
|
+
// stop hammering the socket (failed attempts just flip status
|
|
221
|
+
// disconnected↔connecting and light up every connection indicator) and
|
|
222
|
+
// wait for the `online` event to resume immediately — no backoff wait.
|
|
223
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
224
|
+
this.onlineListener = this.handleOnline.bind(this);
|
|
225
|
+
this.offlineListener = this.handleOffline.bind(this);
|
|
226
|
+
window.addEventListener("online", this.onlineListener);
|
|
227
|
+
window.addEventListener("offline", this.offlineListener);
|
|
228
|
+
}
|
|
229
|
+
|
|
213
230
|
if (this.shouldConnect) {
|
|
214
231
|
this.connect();
|
|
215
232
|
}
|
|
216
233
|
}
|
|
217
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Whether the device currently believes it has network connectivity.
|
|
237
|
+
*
|
|
238
|
+
* Treats "unknown" as online: in Node and other non-browser environments
|
|
239
|
+
* `navigator` (or `navigator.onLine`) is absent, and we must not gate
|
|
240
|
+
* reconnection there — only the browser exposes a trustworthy signal.
|
|
241
|
+
*/
|
|
242
|
+
get isOnline(): boolean {
|
|
243
|
+
return typeof navigator === "undefined" || navigator.onLine !== false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
handleOnline() {
|
|
247
|
+
// Network came back — resume immediately if we still want a connection
|
|
248
|
+
// and aren't already connected. Bypasses the backoff that a pending
|
|
249
|
+
// retryer would otherwise wait out.
|
|
250
|
+
if (this.shouldConnect && this.status !== WebSocketStatus.Connected) {
|
|
251
|
+
this.connect();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
handleOffline() {
|
|
256
|
+
// Stop attempting while offline, but preserve `shouldConnect` so intent
|
|
257
|
+
// survives — `handleOnline`/attach will resume once connectivity returns.
|
|
258
|
+
if (this.cancelWebsocketRetry) {
|
|
259
|
+
this.cancelWebsocketRetry();
|
|
260
|
+
this.cancelWebsocketRetry = undefined;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
this.webSocket?.close();
|
|
264
|
+
this.messageQueue = [];
|
|
265
|
+
} catch (e) {
|
|
266
|
+
console.error(e);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
218
270
|
receivedOnOpenPayload?: Event | undefined = undefined;
|
|
219
271
|
|
|
220
272
|
async onOpen(event: Event) {
|
|
@@ -271,6 +323,14 @@ export class AbracadabraWS extends EventEmitter {
|
|
|
271
323
|
return;
|
|
272
324
|
}
|
|
273
325
|
|
|
326
|
+
// Don't attempt while the device is offline — record the intent and let
|
|
327
|
+
// the `online` event drive the resume. Avoids the reconnect storm of
|
|
328
|
+
// instantly-failing socket attempts against a known-dead network.
|
|
329
|
+
if (!this.isOnline) {
|
|
330
|
+
this.shouldConnect = true;
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
274
334
|
// Always cancel any previously initiated connection retryer instances
|
|
275
335
|
if (this.cancelWebsocketRetry) {
|
|
276
336
|
this.cancelWebsocketRetry();
|
|
@@ -548,8 +608,11 @@ export class AbracadabraWS extends EventEmitter {
|
|
|
548
608
|
this.emit("rateLimited");
|
|
549
609
|
}
|
|
550
610
|
|
|
551
|
-
// trigger connect if no retry is running and we want to have a connection
|
|
552
|
-
|
|
611
|
+
// trigger connect if no retry is running and we want to have a connection.
|
|
612
|
+
// Skip scheduling entirely while offline — `handleOnline` resumes the
|
|
613
|
+
// moment connectivity returns, so there's no point burning a timer (and a
|
|
614
|
+
// failed attempt) against a dead network.
|
|
615
|
+
if (!this.cancelWebsocketRetry && this.shouldConnect && this.isOnline) {
|
|
553
616
|
// Apply a much longer delay for rate-limited closes to let the server window reset.
|
|
554
617
|
const delay = isRateLimited ? 60_000 : this.configuration.delay;
|
|
555
618
|
setTimeout(() => {
|
|
@@ -565,6 +628,13 @@ export class AbracadabraWS extends EventEmitter {
|
|
|
565
628
|
|
|
566
629
|
this.emit("destroy");
|
|
567
630
|
|
|
631
|
+
if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
|
|
632
|
+
if (this.onlineListener) window.removeEventListener("online", this.onlineListener);
|
|
633
|
+
if (this.offlineListener) window.removeEventListener("offline", this.offlineListener);
|
|
634
|
+
}
|
|
635
|
+
this.onlineListener = null;
|
|
636
|
+
this.offlineListener = null;
|
|
637
|
+
|
|
568
638
|
clearInterval(this.intervals.connectionChecker);
|
|
569
639
|
|
|
570
640
|
// If there is still a connection attempt outstanding then we should stop
|
|
@@ -34,7 +34,7 @@ import { E2EAbracadabraProvider } from "./E2EAbracadabraProvider.ts";
|
|
|
34
34
|
export interface BackgroundSyncManagerOptions {
|
|
35
35
|
/** Max parallel WS connections for background sync. Default: 2. */
|
|
36
36
|
concurrency?: number;
|
|
37
|
-
/** Timeout (ms) waiting for a provider to sync. Default:
|
|
37
|
+
/** Timeout (ms) waiting for a provider to sync. Default: 30 000. */
|
|
38
38
|
syncTimeout?: number;
|
|
39
39
|
/** Pre-cache file blobs after syncing a doc. Default: true. */
|
|
40
40
|
prefetchFiles?: boolean;
|
|
@@ -86,6 +86,50 @@ function isValidDocId(id: string): boolean {
|
|
|
86
86
|
return UUID_RE.test(id);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Consecutive transient failures before a doc is promoted from the calm
|
|
91
|
+
* `retrying` status to a genuine `error`. Keeps a single timeout/network blip
|
|
92
|
+
* from flashing a red error badge — the doc only "counts" as broken once it
|
|
93
|
+
* has failed this many passes in a row.
|
|
94
|
+
*/
|
|
95
|
+
const FAILURE_THRESHOLD = 3;
|
|
96
|
+
|
|
97
|
+
/** Delay before a one-shot follow-up retry of stragglers after `syncAll()`. */
|
|
98
|
+
const STRAGGLER_RETRY_DELAY_MS = 60_000;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Decide whether a sync failure is permanent (the server will never accept
|
|
102
|
+
* this doc — deleted, forbidden, or an unparseable id) or merely transient
|
|
103
|
+
* (timeout, dropped socket, momentary 5xx). Permanent failures are parked as
|
|
104
|
+
* `unavailable` and never retried; transient ones go through `retrying`.
|
|
105
|
+
*
|
|
106
|
+
* Best-effort: inspects a numeric `status`/`code` if the thrown value carries
|
|
107
|
+
* one, otherwise falls back to message keywords. Defaults to transient so we
|
|
108
|
+
* never give up on a doc we're unsure about.
|
|
109
|
+
*/
|
|
110
|
+
function isPermanentSyncError(err: unknown): boolean {
|
|
111
|
+
const status =
|
|
112
|
+
typeof err === "object" && err !== null
|
|
113
|
+
? Number(
|
|
114
|
+
(err as { status?: unknown; statusCode?: unknown; code?: unknown })
|
|
115
|
+
.status ??
|
|
116
|
+
(err as { statusCode?: unknown }).statusCode ??
|
|
117
|
+
(err as { code?: unknown }).code,
|
|
118
|
+
)
|
|
119
|
+
: NaN;
|
|
120
|
+
if (status === 403 || status === 404 || status === 410 || status === 422) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
124
|
+
return (
|
|
125
|
+
/\b(403|404|410|422)\b/.test(msg) ||
|
|
126
|
+
msg.includes("forbidden") ||
|
|
127
|
+
msg.includes("not found") ||
|
|
128
|
+
msg.includes("does not exist") ||
|
|
129
|
+
msg.includes("unprocessable")
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
89
133
|
export class BackgroundSyncManager extends EventEmitter {
|
|
90
134
|
private readonly rootProvider: AbracadabraProvider;
|
|
91
135
|
private readonly client: AbracadabraClient;
|
|
@@ -100,6 +144,8 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
100
144
|
|
|
101
145
|
private _destroyed = false;
|
|
102
146
|
private _initPromise: Promise<void> | null = null;
|
|
147
|
+
/** One-shot timer for the post-syncAll straggler retry pass. */
|
|
148
|
+
private _stragglerTimer: ReturnType<typeof setTimeout> | null = null;
|
|
103
149
|
|
|
104
150
|
constructor(
|
|
105
151
|
rootProvider: AbracadabraProvider,
|
|
@@ -113,7 +159,7 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
113
159
|
this.fileBlobStore = fileBlobStore ?? null;
|
|
114
160
|
this.opts = {
|
|
115
161
|
concurrency: opts?.concurrency ?? 2,
|
|
116
|
-
syncTimeout: opts?.syncTimeout ??
|
|
162
|
+
syncTimeout: opts?.syncTimeout ?? 30_000,
|
|
117
163
|
prefetchFiles: opts?.prefetchFiles ?? true,
|
|
118
164
|
throttleMs: opts?.throttleMs ?? 200,
|
|
119
165
|
maxRetries: opts?.maxRetries ?? 2,
|
|
@@ -224,6 +270,38 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
224
270
|
const retryWorkers = Array.from({ length: this.opts.concurrency }, () => retryNext());
|
|
225
271
|
await Promise.all(retryWorkers);
|
|
226
272
|
}
|
|
273
|
+
|
|
274
|
+
// Any docs still failing after the in-run retries are "stragglers" — the
|
|
275
|
+
// usual 1-2 slow docs that just needed a moment. Rather than make the
|
|
276
|
+
// user stare at them until the next periodic pass (which can be far off),
|
|
277
|
+
// schedule one quiet follow-up attempt while the connection is still warm.
|
|
278
|
+
if (failed.length > 0) {
|
|
279
|
+
this._scheduleStragglerRetry(failed.slice());
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Schedule a single delayed retry of the given docs. Replaces any pending
|
|
285
|
+
* straggler timer so repeated `syncAll()` calls don't stack timers.
|
|
286
|
+
*/
|
|
287
|
+
private _scheduleStragglerRetry(docIds: string[]): void {
|
|
288
|
+
if (this._destroyed || docIds.length === 0) return;
|
|
289
|
+
if (this._stragglerTimer) clearTimeout(this._stragglerTimer);
|
|
290
|
+
this._stragglerTimer = setTimeout(() => {
|
|
291
|
+
this._stragglerTimer = null;
|
|
292
|
+
if (this._destroyed) return;
|
|
293
|
+
void (async () => {
|
|
294
|
+
const treeMap = this.rootProvider.document.getMap(
|
|
295
|
+
"doc-tree",
|
|
296
|
+
) as Y.Map<any>;
|
|
297
|
+
for (const docId of docIds) {
|
|
298
|
+
if (this._destroyed) return;
|
|
299
|
+
const entry = treeMap.get(docId);
|
|
300
|
+
const updatedAt = entry?.updatedAt ?? entry?.createdAt ?? 0;
|
|
301
|
+
await this._syncWithSemaphore(docId, updatedAt);
|
|
302
|
+
}
|
|
303
|
+
})();
|
|
304
|
+
}, STRAGGLER_RETRY_DELAY_MS);
|
|
227
305
|
}
|
|
228
306
|
|
|
229
307
|
/** Sync a single document by ID. */
|
|
@@ -299,6 +377,10 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
299
377
|
|
|
300
378
|
destroy(): void {
|
|
301
379
|
this._destroyed = true;
|
|
380
|
+
if (this._stragglerTimer) {
|
|
381
|
+
clearTimeout(this._stragglerTimer);
|
|
382
|
+
this._stragglerTimer = null;
|
|
383
|
+
}
|
|
302
384
|
this.removeAllListeners();
|
|
303
385
|
}
|
|
304
386
|
|
|
@@ -331,23 +413,29 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
331
413
|
}
|
|
332
414
|
}
|
|
333
415
|
|
|
334
|
-
const items: Sortable[] =
|
|
416
|
+
const items: Sortable[] = [];
|
|
417
|
+
for (const [docId, v] of filtered) {
|
|
335
418
|
const state = this.syncStates.get(docId);
|
|
419
|
+
|
|
420
|
+
// Permanently unavailable docs are never queued — the server would
|
|
421
|
+
// just reject them again. They stay parked until a resync clears them.
|
|
422
|
+
if (state?.status === "unavailable") continue;
|
|
423
|
+
|
|
336
424
|
const updatedAt: number = v?.updatedAt ?? v?.createdAt ?? 0;
|
|
337
425
|
|
|
338
426
|
let priority: number;
|
|
339
427
|
if (!state || state.status === "pending") {
|
|
340
428
|
// Never synced — high priority (large number so it sorts first when DESC)
|
|
341
429
|
priority = Number.MAX_SAFE_INTEGER - updatedAt;
|
|
342
|
-
} else if (state.status === "error") {
|
|
343
|
-
//
|
|
430
|
+
} else if (state.status === "error" || state.status === "retrying") {
|
|
431
|
+
// Failing — lowest priority (large negative offset)
|
|
344
432
|
priority = -1;
|
|
345
433
|
} else {
|
|
346
434
|
// Synced — sort by updatedAt desc (most recent = highest priority)
|
|
347
435
|
priority = updatedAt;
|
|
348
436
|
}
|
|
349
|
-
|
|
350
|
-
}
|
|
437
|
+
items.push({ docId, priority });
|
|
438
|
+
}
|
|
351
439
|
|
|
352
440
|
// Sort descending by priority
|
|
353
441
|
items.sort((a, b) => b.priority - a.priority);
|
|
@@ -361,10 +449,18 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
361
449
|
): Promise<boolean> {
|
|
362
450
|
if (this._destroyed) return true;
|
|
363
451
|
|
|
452
|
+
const existing = this.syncStates.get(docId);
|
|
453
|
+
|
|
454
|
+
// Permanently unavailable (deleted/forbidden) — don't keep hammering a
|
|
455
|
+
// doc the server will never accept. Re-emit so the UI keeps the state.
|
|
456
|
+
if (existing && existing.status === "unavailable") {
|
|
457
|
+
this.emit("stateChanged", { docId, state: existing });
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
|
|
364
461
|
// Skip if already synced and doc hasn't changed since last sync.
|
|
365
462
|
// The 200ms grace margin handles edge cases where the updatedAt
|
|
366
463
|
// observer writes a timestamp milliseconds after lastSynced was recorded.
|
|
367
|
-
const existing = this.syncStates.get(docId);
|
|
368
464
|
if (
|
|
369
465
|
existing &&
|
|
370
466
|
existing.status === "synced" &&
|
|
@@ -381,13 +477,28 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
381
477
|
this.syncStates.set(docId, state);
|
|
382
478
|
await this.persistence.setState(state).catch(() => null);
|
|
383
479
|
this.emit("stateChanged", { docId, state });
|
|
384
|
-
|
|
480
|
+
// `retrying` and `error` are both failures that warrant another pass;
|
|
481
|
+
// `unavailable` is permanent so we report it as done (no retry).
|
|
482
|
+
return state.status !== "error" && state.status !== "retrying";
|
|
385
483
|
} finally {
|
|
386
484
|
this.semaphore.release();
|
|
387
485
|
}
|
|
388
486
|
}
|
|
389
487
|
|
|
390
488
|
private async _doSyncDoc(docId: string): Promise<DocSyncState> {
|
|
489
|
+
// Non-UUID ids the server can't parse (422) are permanently unsyncable —
|
|
490
|
+
// park them as `unavailable` rather than churning through retries.
|
|
491
|
+
if (!isValidDocId(docId)) {
|
|
492
|
+
return {
|
|
493
|
+
docId,
|
|
494
|
+
status: "unavailable",
|
|
495
|
+
lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
|
|
496
|
+
error: "Invalid document id (not a UUID)",
|
|
497
|
+
consecutiveFailures: 0,
|
|
498
|
+
isE2E: false,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
391
502
|
// Mark as syncing
|
|
392
503
|
const syncing: DocSyncState = {
|
|
393
504
|
docId,
|
|
@@ -415,11 +526,32 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
415
526
|
}
|
|
416
527
|
} catch (err) {
|
|
417
528
|
const error = err instanceof Error ? err.message : String(err);
|
|
529
|
+
const prev = this.syncStates.get(docId);
|
|
530
|
+
const lastSynced = prev?.lastSynced ?? null;
|
|
531
|
+
|
|
532
|
+
// Permanent failure (deleted / forbidden / unparseable) — park it as
|
|
533
|
+
// `unavailable` so we stop retrying and never surface it as an error.
|
|
534
|
+
if (isPermanentSyncError(err)) {
|
|
535
|
+
return {
|
|
536
|
+
docId,
|
|
537
|
+
status: "unavailable",
|
|
538
|
+
lastSynced,
|
|
539
|
+
error,
|
|
540
|
+
consecutiveFailures: 0,
|
|
541
|
+
isE2E,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Transient failure — escalate to a genuine `error` only once it has
|
|
546
|
+
// recurred past the threshold; otherwise keep it calm as `retrying`.
|
|
547
|
+
const consecutiveFailures = (prev?.consecutiveFailures ?? 0) + 1;
|
|
418
548
|
return {
|
|
419
549
|
docId,
|
|
420
|
-
status:
|
|
421
|
-
|
|
550
|
+
status:
|
|
551
|
+
consecutiveFailures >= FAILURE_THRESHOLD ? "error" : "retrying",
|
|
552
|
+
lastSynced,
|
|
422
553
|
error,
|
|
554
|
+
consecutiveFailures,
|
|
423
555
|
isE2E,
|
|
424
556
|
};
|
|
425
557
|
}
|
|
@@ -445,6 +577,7 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
445
577
|
docId,
|
|
446
578
|
status: "synced",
|
|
447
579
|
lastSynced: Date.now(),
|
|
580
|
+
consecutiveFailures: 0,
|
|
448
581
|
isE2E: false,
|
|
449
582
|
};
|
|
450
583
|
}
|
|
@@ -478,7 +611,13 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
478
611
|
// the observer's timestamp write from triggering a spurious re-sync.
|
|
479
612
|
const treeEntry = treeMap.get(docId);
|
|
480
613
|
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
481
|
-
return {
|
|
614
|
+
return {
|
|
615
|
+
docId,
|
|
616
|
+
status: "synced",
|
|
617
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
618
|
+
consecutiveFailures: 0,
|
|
619
|
+
isE2E: false,
|
|
620
|
+
};
|
|
482
621
|
} finally {
|
|
483
622
|
// Always release the provider if it was created solely for background sync.
|
|
484
623
|
// This closes its IDB database, freeing the file descriptor.
|
|
@@ -499,7 +638,13 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
499
638
|
|
|
500
639
|
if (!docKeyManager || !keystore) {
|
|
501
640
|
// No key management configured — skip silently
|
|
502
|
-
return {
|
|
641
|
+
return {
|
|
642
|
+
docId,
|
|
643
|
+
status: "skipped",
|
|
644
|
+
lastSynced: null,
|
|
645
|
+
consecutiveFailures: 0,
|
|
646
|
+
isE2E: true,
|
|
647
|
+
};
|
|
503
648
|
}
|
|
504
649
|
|
|
505
650
|
const childDoc = new Y.Doc({ guid: docId });
|
|
@@ -535,7 +680,13 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
535
680
|
// the observer's timestamp write from triggering a spurious re-sync.
|
|
536
681
|
const treeEntry = treeMap.get(docId);
|
|
537
682
|
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
538
|
-
return {
|
|
683
|
+
return {
|
|
684
|
+
docId,
|
|
685
|
+
status: "synced",
|
|
686
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
687
|
+
consecutiveFailures: 0,
|
|
688
|
+
isE2E: true,
|
|
689
|
+
};
|
|
539
690
|
} finally {
|
|
540
691
|
childProvider.destroy();
|
|
541
692
|
}
|
|
@@ -8,14 +8,47 @@
|
|
|
8
8
|
* Falls back to a no-op when IndexedDB is unavailable (SSR / Node.js).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Background-sync lifecycle status.
|
|
13
|
+
*
|
|
14
|
+
* - `pending` — never synced; queued for first attempt.
|
|
15
|
+
* - `syncing` — attempt in progress (transient, not persisted as terminal).
|
|
16
|
+
* - `synced` — last attempt succeeded; `lastSynced` is set.
|
|
17
|
+
* - `retrying` — a *transient* failure (timeout, network) that has NOT yet
|
|
18
|
+
* crossed the failure threshold. Will be retried silently;
|
|
19
|
+
* the UI should treat this calmly and NOT count it as an error.
|
|
20
|
+
* - `error` — a transient failure that has recurred past the threshold
|
|
21
|
+
* (`consecutiveFailures >= FAILURE_THRESHOLD`). Genuinely
|
|
22
|
+
* needs attention; this is what the UI surfaces as red.
|
|
23
|
+
* - `unavailable` — a *permanent* failure (doc deleted/forbidden/invalid id —
|
|
24
|
+
* 404/403/410/422). Not retried, and not surfaced as an error.
|
|
25
|
+
* - `skipped` — E2E doc we can't sync (no keystore). Benign.
|
|
26
|
+
*/
|
|
27
|
+
export type DocSyncStatus =
|
|
28
|
+
| "pending"
|
|
29
|
+
| "syncing"
|
|
30
|
+
| "synced"
|
|
31
|
+
| "retrying"
|
|
32
|
+
| "error"
|
|
33
|
+
| "unavailable"
|
|
34
|
+
| "skipped";
|
|
35
|
+
|
|
11
36
|
export interface DocSyncState {
|
|
12
37
|
docId: string;
|
|
13
38
|
/** Current lifecycle status of this document's background sync. */
|
|
14
|
-
status:
|
|
39
|
+
status: DocSyncStatus;
|
|
15
40
|
/** Unix ms of the last successful sync, or null if never synced. */
|
|
16
41
|
lastSynced: number | null;
|
|
17
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Human-readable message for the most recent failure. Populated for
|
|
44
|
+
* `retrying`, `error`, and `unavailable` — cleared on a successful sync.
|
|
45
|
+
*/
|
|
18
46
|
error?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Count of consecutive failed attempts. Reset to 0 on success. Drives the
|
|
49
|
+
* `retrying` → `error` escalation so a single transient blip never nags.
|
|
50
|
+
*/
|
|
51
|
+
consecutiveFailures?: number;
|
|
19
52
|
/** Whether the document uses E2E encryption. */
|
|
20
53
|
isE2E: boolean;
|
|
21
54
|
}
|
package/src/index.ts
CHANGED
|
@@ -31,7 +31,10 @@ export { attachUpdatedAtObserver } from "./TreeTimestamps.ts";
|
|
|
31
31
|
export { BackgroundSyncManager } from "./BackgroundSyncManager.ts";
|
|
32
32
|
export type { BackgroundSyncManagerOptions } from "./BackgroundSyncManager.ts";
|
|
33
33
|
export { BackgroundSyncPersistence } from "./BackgroundSyncPersistence.ts";
|
|
34
|
-
export type {
|
|
34
|
+
export type {
|
|
35
|
+
DocSyncState,
|
|
36
|
+
DocSyncStatus,
|
|
37
|
+
} from "./BackgroundSyncPersistence.ts";
|
|
35
38
|
export * from "./webrtc/index.ts";
|
|
36
39
|
export { BroadcastChannelSync } from "./sync/BroadcastChannelSync.ts";
|
|
37
40
|
export { IdentityDocProvider, deriveIdentityDocId } from "./IdentityDoc.ts";
|