@abraca/dabra 1.5.0 → 1.8.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 +478 -42
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +477 -43
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +234 -11
- package/package.json +1 -1
- package/src/AbracadabraBaseProvider.ts +15 -0
- package/src/AbracadabraProvider.ts +56 -0
- package/src/BackgroundSyncManager.ts +44 -30
- package/src/ChatClient.ts +234 -0
- package/src/IdentityDoc.ts +11 -0
- package/src/NotificationsClient.ts +185 -0
- package/src/TreeTimestamps.ts +47 -16
- package/src/index.ts +16 -0
- package/src/types.ts +92 -0
- package/src/webrtc/DevicePairingChannel.ts +18 -6
package/dist/index.d.ts
CHANGED
|
@@ -811,6 +811,12 @@ declare class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
811
811
|
private childProviders;
|
|
812
812
|
private pendingLoads;
|
|
813
813
|
private subdocLoading;
|
|
814
|
+
/** LRU tracking: childId → last access timestamp */
|
|
815
|
+
private childAccessTimes;
|
|
816
|
+
/** Pinned children that must not be evicted (e.g. actively viewed docs) */
|
|
817
|
+
private pinnedChildren;
|
|
818
|
+
/** Max simultaneously cached child providers before LRU eviction kicks in */
|
|
819
|
+
private static readonly MAX_CHILDREN;
|
|
814
820
|
private abracadabraConfig;
|
|
815
821
|
private readonly boundHandleYSubdocsChange;
|
|
816
822
|
/**
|
|
@@ -884,6 +890,20 @@ declare class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
884
890
|
loadChild(childId: string): Promise<AbracadabraProvider>;
|
|
885
891
|
private _doLoadChild;
|
|
886
892
|
unloadChild(childId: string): void;
|
|
893
|
+
/**
|
|
894
|
+
* Mark a child as pinned so LRU eviction will not remove it.
|
|
895
|
+
* Use this when a document is actively being viewed by the user.
|
|
896
|
+
*/
|
|
897
|
+
pinChild(childId: string): void;
|
|
898
|
+
/**
|
|
899
|
+
* Unpin a child, allowing LRU eviction to reclaim it when capacity is exceeded.
|
|
900
|
+
*/
|
|
901
|
+
unpinChild(childId: string): void;
|
|
902
|
+
/**
|
|
903
|
+
* Evict least-recently-used unpinned child providers until the cache is
|
|
904
|
+
* at or below MAX_CHILDREN.
|
|
905
|
+
*/
|
|
906
|
+
private evictLRU;
|
|
887
907
|
/** Return all currently-loaded child providers. */
|
|
888
908
|
get children(): Map<string, AbracadabraProvider>;
|
|
889
909
|
/**
|
|
@@ -1133,6 +1153,11 @@ type onAwarenessChangeParameters = {
|
|
|
1133
1153
|
type onStatelessParameters = {
|
|
1134
1154
|
payload: string;
|
|
1135
1155
|
};
|
|
1156
|
+
type onServerErrorParameters = {
|
|
1157
|
+
source: string;
|
|
1158
|
+
code: string;
|
|
1159
|
+
message: string;
|
|
1160
|
+
};
|
|
1136
1161
|
type StatesArray = {
|
|
1137
1162
|
clientId: number;
|
|
1138
1163
|
[key: string | number]: any;
|
|
@@ -1319,6 +1344,77 @@ interface DocEncryptionInfo {
|
|
|
1319
1344
|
effective_mode: "none" | "cse" | "e2e";
|
|
1320
1345
|
inherited_from?: string;
|
|
1321
1346
|
}
|
|
1347
|
+
interface ChatMessage {
|
|
1348
|
+
id: string;
|
|
1349
|
+
channel: string;
|
|
1350
|
+
sender_id: string;
|
|
1351
|
+
sender_name?: string | null;
|
|
1352
|
+
content: string;
|
|
1353
|
+
created_at: number;
|
|
1354
|
+
}
|
|
1355
|
+
interface ChatChannel {
|
|
1356
|
+
channel: string;
|
|
1357
|
+
label?: string | null;
|
|
1358
|
+
last_message: ChatMessage;
|
|
1359
|
+
unread_count: number;
|
|
1360
|
+
}
|
|
1361
|
+
interface ChatTypingEvent {
|
|
1362
|
+
channel: string;
|
|
1363
|
+
sender_id: string;
|
|
1364
|
+
sender_name?: string | null;
|
|
1365
|
+
}
|
|
1366
|
+
interface ChatReadReceipt {
|
|
1367
|
+
channel: string;
|
|
1368
|
+
user_id: string;
|
|
1369
|
+
last_read_at: number;
|
|
1370
|
+
}
|
|
1371
|
+
interface ChatReadCursor {
|
|
1372
|
+
user_id: string;
|
|
1373
|
+
last_read_at: number;
|
|
1374
|
+
}
|
|
1375
|
+
interface SendChatMessageInput {
|
|
1376
|
+
channel: string;
|
|
1377
|
+
content: string;
|
|
1378
|
+
sender_name?: string;
|
|
1379
|
+
}
|
|
1380
|
+
interface GetChatHistoryInput {
|
|
1381
|
+
channel: string;
|
|
1382
|
+
before?: number;
|
|
1383
|
+
limit?: number;
|
|
1384
|
+
}
|
|
1385
|
+
interface NotificationRecord {
|
|
1386
|
+
id: string;
|
|
1387
|
+
recipient_id: string;
|
|
1388
|
+
notification_type: string;
|
|
1389
|
+
title: string;
|
|
1390
|
+
body: string;
|
|
1391
|
+
icon?: string | null;
|
|
1392
|
+
link?: string | null;
|
|
1393
|
+
source_id?: string | null;
|
|
1394
|
+
read: boolean;
|
|
1395
|
+
created_at: number;
|
|
1396
|
+
}
|
|
1397
|
+
interface NotificationReadUpdate {
|
|
1398
|
+
/** Present when a specific set of notifications was marked read. */
|
|
1399
|
+
ids?: string[];
|
|
1400
|
+
/** Present when mark_all_read was called. */
|
|
1401
|
+
all?: boolean;
|
|
1402
|
+
recipient_id: string;
|
|
1403
|
+
}
|
|
1404
|
+
interface CreateNotificationInput {
|
|
1405
|
+
recipient_id: string;
|
|
1406
|
+
notification_type?: string;
|
|
1407
|
+
title: string;
|
|
1408
|
+
body?: string;
|
|
1409
|
+
icon?: string;
|
|
1410
|
+
link?: string;
|
|
1411
|
+
source_id?: string;
|
|
1412
|
+
}
|
|
1413
|
+
interface FetchNotificationsInput {
|
|
1414
|
+
before?: number;
|
|
1415
|
+
limit?: number;
|
|
1416
|
+
unread_only?: boolean;
|
|
1417
|
+
}
|
|
1322
1418
|
//#endregion
|
|
1323
1419
|
//#region packages/provider/src/AbracadabraWS.d.ts
|
|
1324
1420
|
type AbracadabraWebSocketConn = WebSocket & {
|
|
@@ -1499,6 +1595,7 @@ interface CompleteAbracadabraBaseProviderConfiguration {
|
|
|
1499
1595
|
onAwarenessUpdate: (data: onAwarenessUpdateParameters) => void;
|
|
1500
1596
|
onAwarenessChange: (data: onAwarenessChangeParameters) => void;
|
|
1501
1597
|
onStateless: (data: onStatelessParameters) => void;
|
|
1598
|
+
onServerError: (data: onServerErrorParameters) => void;
|
|
1502
1599
|
onUnsyncedChanges: (data: onUnsyncedChangesParameters) => void;
|
|
1503
1600
|
}
|
|
1504
1601
|
/** @deprecated Use CompleteAbracadabraBaseProviderConfiguration */
|
|
@@ -1812,19 +1909,26 @@ declare function makeEncryptedYText(ydoc: Y.Doc, fieldName: string, docKey: Cryp
|
|
|
1812
1909
|
//#endregion
|
|
1813
1910
|
//#region packages/provider/src/TreeTimestamps.d.ts
|
|
1814
1911
|
/**
|
|
1815
|
-
* Attach an observer that writes `updatedAt
|
|
1816
|
-
*
|
|
1817
|
-
* non-offline update.
|
|
1912
|
+
* Attach an observer that writes `updatedAt` to the root doc-tree entry for
|
|
1913
|
+
* `childDocId` whenever the child doc receives a non-offline update.
|
|
1818
1914
|
*
|
|
1819
|
-
*
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
1915
|
+
* Writes are throttled: the first qualifying update records the timestamp;
|
|
1916
|
+
* a trailing-edge timer flushes it to the tree map after `throttleMs`.
|
|
1917
|
+
*
|
|
1918
|
+
* @param treeMap The root doc's "doc-tree" Y.Map.
|
|
1919
|
+
* @param childDocId The child document's UUID (key in treeMap).
|
|
1920
|
+
* @param childDoc The child Y.Doc to observe.
|
|
1822
1921
|
* @param offlineStore The child provider's OfflineStore (used to detect
|
|
1823
1922
|
* offline-replay origins and skip them). Pass null when
|
|
1824
1923
|
* the offline store is disabled.
|
|
1825
|
-
* @
|
|
1924
|
+
* @param options Optional config. `throttleMs` controls the write
|
|
1925
|
+
* interval (default 5000).
|
|
1926
|
+
* @returns Cleanup function — call on provider destroy. Flushes
|
|
1927
|
+
* any pending write before detaching.
|
|
1826
1928
|
*/
|
|
1827
|
-
declare function attachUpdatedAtObserver(treeMap: Y.Map<any>, childDocId: string, childDoc: Y.Doc, offlineStore: OfflineStore | null
|
|
1929
|
+
declare function attachUpdatedAtObserver(treeMap: Y.Map<any>, childDocId: string, childDoc: Y.Doc, offlineStore: OfflineStore | null, options?: {
|
|
1930
|
+
throttleMs?: number;
|
|
1931
|
+
}): () => void;
|
|
1828
1932
|
//#endregion
|
|
1829
1933
|
//#region packages/provider/src/BackgroundSyncPersistence.d.ts
|
|
1830
1934
|
/**
|
|
@@ -2501,14 +2605,22 @@ declare class DevicePairingChannel extends EventEmitter {
|
|
|
2501
2605
|
/**
|
|
2502
2606
|
* Approve the pending pairing request. Calls `client.addKey()` to
|
|
2503
2607
|
* register Device B's public key, then notifies Device B.
|
|
2608
|
+
*
|
|
2609
|
+
* @param client Authenticated REST client.
|
|
2610
|
+
* @param masterPublicKey Approver's account-level Ed25519 public key (base64url).
|
|
2611
|
+
* Sent to Device B so it can adopt the master's identity doc.
|
|
2504
2612
|
*/
|
|
2505
|
-
approve(client: AbracadabraClient): Promise<PairingResult>;
|
|
2613
|
+
approve(client: AbracadabraClient, masterPublicKey?: string): Promise<PairingResult>;
|
|
2506
2614
|
/**
|
|
2507
2615
|
* Approve via server-side device invite. Creates a single-use invite code
|
|
2508
2616
|
* and sends it to Device B over the E2EE channel. Device B redeems it
|
|
2509
2617
|
* independently via HTTP — Device A can go offline after this.
|
|
2618
|
+
*
|
|
2619
|
+
* @param client Authenticated REST client.
|
|
2620
|
+
* @param masterPublicKey Approver's account-level Ed25519 public key (base64url).
|
|
2621
|
+
* Sent to Device B so it can adopt the master's identity doc.
|
|
2510
2622
|
*/
|
|
2511
|
-
approveWithInvite(client: AbracadabraClient): Promise<PairingResult>;
|
|
2623
|
+
approveWithInvite(client: AbracadabraClient, masterPublicKey?: string): Promise<PairingResult>;
|
|
2512
2624
|
/**
|
|
2513
2625
|
* Reject the pending pairing request.
|
|
2514
2626
|
*/
|
|
@@ -2728,6 +2840,12 @@ declare class IdentityDocProvider extends EventEmitter {
|
|
|
2728
2840
|
* Call this to decide whether to run migration from localStorage.
|
|
2729
2841
|
*/
|
|
2730
2842
|
isEmpty(): boolean;
|
|
2843
|
+
/**
|
|
2844
|
+
* Enable WebRTC P2P sync at runtime.
|
|
2845
|
+
* Use this for claimed/passkey users where E2EE identity derivation
|
|
2846
|
+
* was deferred to avoid biometric prompts on page load.
|
|
2847
|
+
*/
|
|
2848
|
+
enableWebRTC(webrtcConfig: NonNullable<IdentityDocConfiguration["webrtc"]>): void;
|
|
2731
2849
|
/**
|
|
2732
2850
|
* Update the sync server URL at runtime (e.g. when user changes their
|
|
2733
2851
|
* designated sync server in settings).
|
|
@@ -2793,6 +2911,111 @@ declare class DeviceRegistrationService {
|
|
|
2793
2911
|
}): Promise<void>;
|
|
2794
2912
|
}
|
|
2795
2913
|
//#endregion
|
|
2914
|
+
//#region packages/provider/src/ChatClient.d.ts
|
|
2915
|
+
/**
|
|
2916
|
+
* Minimal provider surface ChatClient needs. Matches `AbracadabraBaseProvider`.
|
|
2917
|
+
* Kept as an interface so consumers can pass any compatible transport.
|
|
2918
|
+
*/
|
|
2919
|
+
interface ChatClientTransport {
|
|
2920
|
+
sendStateless(payload: string): void;
|
|
2921
|
+
on(event: string, fn: Function): unknown;
|
|
2922
|
+
off(event: string, fn?: Function): unknown;
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Typed client for the Abracadabra chat feature.
|
|
2926
|
+
*
|
|
2927
|
+
* Wraps a connected provider (or base provider) and translates JSON envelopes
|
|
2928
|
+
* on the stateless channel into typed method calls and events.
|
|
2929
|
+
*
|
|
2930
|
+
* Events emitted:
|
|
2931
|
+
* - `message` → ChatMessage (new message broadcast)
|
|
2932
|
+
* - `typing` → ChatTypingEvent (typing indicator broadcast)
|
|
2933
|
+
* - `readReceipt` → ChatReadReceipt (mark_read broadcast)
|
|
2934
|
+
*/
|
|
2935
|
+
declare class ChatClient extends EventEmitter {
|
|
2936
|
+
private readonly provider;
|
|
2937
|
+
private readonly responseTimeoutMs;
|
|
2938
|
+
private readonly pending;
|
|
2939
|
+
private readonly boundOnStateless;
|
|
2940
|
+
constructor(provider: ChatClientTransport, options?: {
|
|
2941
|
+
responseTimeoutMs?: number;
|
|
2942
|
+
});
|
|
2943
|
+
/** Stop listening for chat messages. Does not disconnect the underlying provider. */
|
|
2944
|
+
destroy(): void;
|
|
2945
|
+
/** Send a chat message on a channel. Fire-and-forget (the server broadcasts). */
|
|
2946
|
+
sendMessage(input: SendChatMessageInput): void;
|
|
2947
|
+
/** Fetch historical messages for a channel. Resolves with the server response. */
|
|
2948
|
+
getHistory(input: GetChatHistoryInput): Promise<{
|
|
2949
|
+
channel: string;
|
|
2950
|
+
messages: ChatMessage[];
|
|
2951
|
+
}>;
|
|
2952
|
+
/** Broadcast a typing indicator on a channel. */
|
|
2953
|
+
sendTyping(channel: string): void;
|
|
2954
|
+
/** List the current user's channels (ordered by last activity). */
|
|
2955
|
+
listChannels(): Promise<{
|
|
2956
|
+
channels: ChatChannel[];
|
|
2957
|
+
}>;
|
|
2958
|
+
/** Mark a channel read up to `timestamp` (unix ms). */
|
|
2959
|
+
markRead(channel: string, timestamp: number): void;
|
|
2960
|
+
/** Fetch per-user read cursors for a channel. */
|
|
2961
|
+
getReadCursors(channel: string): Promise<{
|
|
2962
|
+
channel: string;
|
|
2963
|
+
cursors: ChatReadCursor[];
|
|
2964
|
+
}>;
|
|
2965
|
+
onMessage(fn: (m: ChatMessage) => void): this;
|
|
2966
|
+
onTyping(fn: (e: ChatTypingEvent) => void): this;
|
|
2967
|
+
onReadReceipt(fn: (e: ChatReadReceipt) => void): this;
|
|
2968
|
+
private enqueue;
|
|
2969
|
+
private removePending;
|
|
2970
|
+
private resolveNext;
|
|
2971
|
+
private handleStateless;
|
|
2972
|
+
}
|
|
2973
|
+
//#endregion
|
|
2974
|
+
//#region packages/provider/src/NotificationsClient.d.ts
|
|
2975
|
+
/**
|
|
2976
|
+
* Typed client for the Abracadabra notifications feature.
|
|
2977
|
+
*
|
|
2978
|
+
* Emits:
|
|
2979
|
+
* - `new` → NotificationRecord (incoming notify:new broadcast)
|
|
2980
|
+
* - `readUpdate` → NotificationReadUpdate (broadcast after mark_read / mark_all_read)
|
|
2981
|
+
*/
|
|
2982
|
+
declare class NotificationsClient extends EventEmitter {
|
|
2983
|
+
private readonly provider;
|
|
2984
|
+
private readonly responseTimeoutMs;
|
|
2985
|
+
private readonly pending;
|
|
2986
|
+
private readonly boundOnStateless;
|
|
2987
|
+
constructor(provider: ChatClientTransport, options?: {
|
|
2988
|
+
responseTimeoutMs?: number;
|
|
2989
|
+
});
|
|
2990
|
+
destroy(): void;
|
|
2991
|
+
/**
|
|
2992
|
+
* Create a notification targeting a specific recipient. Requires elevated role
|
|
2993
|
+
* (service or admin); a `server:error` event with code `forbidden` is emitted
|
|
2994
|
+
* by the underlying provider if the caller lacks permission.
|
|
2995
|
+
*/
|
|
2996
|
+
create(input: CreateNotificationInput): void;
|
|
2997
|
+
/** Fetch notification history for the current user. */
|
|
2998
|
+
fetch(input?: FetchNotificationsInput): Promise<{
|
|
2999
|
+
notifications: NotificationRecord[];
|
|
3000
|
+
}>;
|
|
3001
|
+
/** Mark a single notification, or a batch, as read. */
|
|
3002
|
+
markRead(target: {
|
|
3003
|
+
id: string;
|
|
3004
|
+
} | {
|
|
3005
|
+
ids: string[];
|
|
3006
|
+
}): void;
|
|
3007
|
+
/** Mark every notification for the current user as read. */
|
|
3008
|
+
markAllRead(): void;
|
|
3009
|
+
/** Mark all notifications generated by the given source (e.g. a chat channel) as read. */
|
|
3010
|
+
markReadBySource(sourceId: string): void;
|
|
3011
|
+
onNew(fn: (n: NotificationRecord) => void): this;
|
|
3012
|
+
onReadUpdate(fn: (u: NotificationReadUpdate) => void): this;
|
|
3013
|
+
private enqueue;
|
|
3014
|
+
private removePending;
|
|
3015
|
+
private resolveNext;
|
|
3016
|
+
private handleStateless;
|
|
3017
|
+
}
|
|
3018
|
+
//#endregion
|
|
2796
3019
|
//#region node_modules/@scure/bip39/esm/wordlists/english.d.ts
|
|
2797
3020
|
declare const wordlist: string[];
|
|
2798
3021
|
//#endregion
|
|
@@ -2860,4 +3083,4 @@ declare function wrapSeed(seed: Uint8Array, wrappingKeyBytes: Uint8Array): Promi
|
|
|
2860
3083
|
*/
|
|
2861
3084
|
declare function unwrapSeed(ciphertext: ArrayBuffer, iv: Uint8Array, wrappingKeyBytes: Uint8Array): Promise<Uint8Array>;
|
|
2862
3085
|
//#endregion
|
|
2863
|
-
export { AbracadabraBaseProvider, AbracadabraBaseProviderConfiguration, AbracadabraClient, AbracadabraClientConfig, AbracadabraOutgoingMessageArguments, AbracadabraProvider, AbracadabraProviderConfiguration, AbracadabraWS, AbracadabraWSConfiguration, AbracadabraWebRTC, type AbracadabraWebRTCConfiguration, AbracadabraWebSocketConn, AuthMessageType, AuthorizedScope, AwarenessError, BackgroundSyncManager, type BackgroundSyncManagerOptions, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, CloseEvent, CompleteAbracadabraBaseProviderConfiguration, CompleteAbracadabraWSConfiguration, CompleteHocuspocusProviderConfiguration, CompleteHocuspocusProviderWebsocketConfiguration, ConnectionTimeout, Constructable, ConstructableOutgoingMessage, CryptoIdentity, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, type DevicePairingConfig, DeviceRegistrationService, type DeviceServerStatus, type DeviceTier, type DocEncryptionInfo, DocKeyManager, type DocSyncState, DocumentCache, type DocumentCacheOptions, DocumentMeta, E2EAbracadabraProvider, E2EEChannel, type E2EEIdentity, E2EOfflineStore, EffectivePermissionEntry, EffectivePermissionsResponse, EffectiveRole, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, type FileTransferMeta, type FileTransferStatus, Forbidden, HealthStatus, HocusPocusWebSocket, HocuspocusProvider, HocuspocusProviderConfiguration, HocuspocusProviderWebsocket, HocuspocusProviderWebsocketConfiguration, HocuspocusWebSocket, type IdentityDeviceEntry, type IdentityDocConfiguration, IdentityDocProvider, type IdentityProfile, type IdentityServerEntry, type IdentitySpaceEntry, InviteRow, KEY_EXCHANGE_CHANNEL, ManualSignaling, type ManualSignalingBlob, MessageTooBig, MessageType, OfflineStore, OutgoingMessageArguments, OutgoingMessageInterface, type PairingRequest, type PairingResult, PeerConnection, type PeerInfo, type PeerState, PendingSubdoc, PermissionEntry, PublicKeyInfo, ResetConnection, SearchIndex, SearchResult, ServerInfo, type SignalingIncoming, type SignalingOutgoing, SignalingSocket, SnapshotCreateResult, SnapshotData, SnapshotForkResult, SnapshotMeta, SnapshotRestoreResult, SpaceMeta, StatesArray, SubdocMessage, SubdocRegisteredEvent, Unauthorized, UploadInfo, UploadMeta, UploadQueueEntry, UploadQueueStatus, UserProfile, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptField, generateMnemonic, makeEncryptedYMap, makeEncryptedYText, mnemonicToEd25519Seed, mnemonicToKeyPair, onAuthenticatedParameters, onAuthenticationFailedParameters, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatelessParameters, onStatusParameters, onSubdocLoadedParameters, onSubdocRegisteredParameters, onSyncedParameters, onUnsyncedChangesParameters, readAuthMessage, unwrapSeed, validateMnemonic, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
|
|
3086
|
+
export { AbracadabraBaseProvider, AbracadabraBaseProviderConfiguration, AbracadabraClient, AbracadabraClientConfig, AbracadabraOutgoingMessageArguments, AbracadabraProvider, AbracadabraProviderConfiguration, AbracadabraWS, AbracadabraWSConfiguration, AbracadabraWebRTC, type AbracadabraWebRTCConfiguration, AbracadabraWebSocketConn, AuthMessageType, AuthorizedScope, AwarenessError, BackgroundSyncManager, type BackgroundSyncManagerOptions, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, type ChatChannel, ChatClient, type ChatClientTransport, type ChatMessage, type ChatReadCursor, type ChatReadReceipt, type ChatTypingEvent, CloseEvent, CompleteAbracadabraBaseProviderConfiguration, CompleteAbracadabraWSConfiguration, CompleteHocuspocusProviderConfiguration, CompleteHocuspocusProviderWebsocketConfiguration, ConnectionTimeout, Constructable, ConstructableOutgoingMessage, type CreateNotificationInput, CryptoIdentity, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, type DevicePairingConfig, DeviceRegistrationService, type DeviceServerStatus, type DeviceTier, type DocEncryptionInfo, DocKeyManager, type DocSyncState, DocumentCache, type DocumentCacheOptions, DocumentMeta, E2EAbracadabraProvider, E2EEChannel, type E2EEIdentity, E2EOfflineStore, EffectivePermissionEntry, EffectivePermissionsResponse, EffectiveRole, EncryptedYMap, EncryptedYText, type FetchNotificationsInput, FileBlobStore, FileTransferChannel, FileTransferHandle, type FileTransferMeta, type FileTransferStatus, Forbidden, type GetChatHistoryInput, HealthStatus, HocusPocusWebSocket, HocuspocusProvider, HocuspocusProviderConfiguration, HocuspocusProviderWebsocket, HocuspocusProviderWebsocketConfiguration, HocuspocusWebSocket, type IdentityDeviceEntry, type IdentityDocConfiguration, IdentityDocProvider, type IdentityProfile, type IdentityServerEntry, type IdentitySpaceEntry, InviteRow, KEY_EXCHANGE_CHANNEL, ManualSignaling, type ManualSignalingBlob, MessageTooBig, MessageType, type NotificationReadUpdate, type NotificationRecord, NotificationsClient, OfflineStore, OutgoingMessageArguments, OutgoingMessageInterface, type PairingRequest, type PairingResult, PeerConnection, type PeerInfo, type PeerState, PendingSubdoc, PermissionEntry, PublicKeyInfo, ResetConnection, SearchIndex, SearchResult, type SendChatMessageInput, ServerInfo, type SignalingIncoming, type SignalingOutgoing, SignalingSocket, SnapshotCreateResult, SnapshotData, SnapshotForkResult, SnapshotMeta, SnapshotRestoreResult, SpaceMeta, StatesArray, SubdocMessage, SubdocRegisteredEvent, Unauthorized, UploadInfo, UploadMeta, UploadQueueEntry, UploadQueueStatus, UserProfile, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptField, generateMnemonic, makeEncryptedYMap, makeEncryptedYText, mnemonicToEd25519Seed, mnemonicToKeyPair, onAuthenticatedParameters, onAuthenticationFailedParameters, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onServerErrorParameters, onStatelessParameters, onStatusParameters, onSubdocLoadedParameters, onSubdocRegisteredParameters, onSyncedParameters, onUnsyncedChangesParameters, readAuthMessage, unwrapSeed, validateMnemonic, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
|
package/package.json
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
onMessageParameters,
|
|
26
26
|
onOpenParameters,
|
|
27
27
|
onOutgoingMessageParameters,
|
|
28
|
+
onServerErrorParameters,
|
|
28
29
|
onStatelessParameters,
|
|
29
30
|
onStatusParameters,
|
|
30
31
|
onSyncedParameters,
|
|
@@ -95,6 +96,7 @@ export interface CompleteAbracadabraBaseProviderConfiguration {
|
|
|
95
96
|
onAwarenessUpdate: (data: onAwarenessUpdateParameters) => void;
|
|
96
97
|
onAwarenessChange: (data: onAwarenessChangeParameters) => void;
|
|
97
98
|
onStateless: (data: onStatelessParameters) => void;
|
|
99
|
+
onServerError: (data: onServerErrorParameters) => void;
|
|
98
100
|
onUnsyncedChanges: (data: onUnsyncedChangesParameters) => void;
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -129,6 +131,7 @@ export class AbracadabraBaseProvider extends EventEmitter {
|
|
|
129
131
|
onAwarenessUpdate: () => null,
|
|
130
132
|
onAwarenessChange: () => null,
|
|
131
133
|
onStateless: () => null,
|
|
134
|
+
onServerError: () => null,
|
|
132
135
|
onUnsyncedChanges: () => null,
|
|
133
136
|
};
|
|
134
137
|
|
|
@@ -172,6 +175,7 @@ export class AbracadabraBaseProvider extends EventEmitter {
|
|
|
172
175
|
this.on("awarenessUpdate", this.configuration.onAwarenessUpdate);
|
|
173
176
|
this.on("awarenessChange", this.configuration.onAwarenessChange);
|
|
174
177
|
this.on("stateless", this.configuration.onStateless);
|
|
178
|
+
this.on("serverError", this.configuration.onServerError);
|
|
175
179
|
this.on("unsyncedChanges", this.configuration.onUnsyncedChanges);
|
|
176
180
|
|
|
177
181
|
this.on("authenticated", this.configuration.onAuthenticated);
|
|
@@ -368,6 +372,17 @@ export class AbracadabraBaseProvider extends EventEmitter {
|
|
|
368
372
|
}
|
|
369
373
|
|
|
370
374
|
receiveStateless(payload: string) {
|
|
375
|
+
try {
|
|
376
|
+
const parsed = JSON.parse(payload);
|
|
377
|
+
if (parsed?.type === "error" && parsed.source && parsed.code) {
|
|
378
|
+
const { source, code, message } = parsed;
|
|
379
|
+
console.warn(`[Abracadabra] Server error: ${source} (${code}) — ${message}`);
|
|
380
|
+
this.emit("serverError", { source, code, message: message ?? "" });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
// not JSON — fall through to generic stateless event
|
|
385
|
+
}
|
|
371
386
|
this.emit("stateless", { payload });
|
|
372
387
|
}
|
|
373
388
|
|
|
@@ -110,6 +110,13 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
110
110
|
private pendingLoads = new Map<string, Promise<AbracadabraProvider>>();
|
|
111
111
|
private subdocLoading: "lazy" | "eager";
|
|
112
112
|
|
|
113
|
+
/** LRU tracking: childId → last access timestamp */
|
|
114
|
+
private childAccessTimes = new Map<string, number>();
|
|
115
|
+
/** Pinned children that must not be evicted (e.g. actively viewed docs) */
|
|
116
|
+
private pinnedChildren = new Set<string>();
|
|
117
|
+
/** Max simultaneously cached child providers before LRU eviction kicks in */
|
|
118
|
+
private static readonly MAX_CHILDREN = 20;
|
|
119
|
+
|
|
113
120
|
private abracadabraConfig: AbracadabraProviderConfiguration;
|
|
114
121
|
|
|
115
122
|
private readonly boundHandleYSubdocsChange = this.handleYSubdocsChange.bind(this);
|
|
@@ -454,6 +461,7 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
454
461
|
}
|
|
455
462
|
|
|
456
463
|
if (this.childProviders.has(childId)) {
|
|
464
|
+
this.childAccessTimes.set(childId, Date.now());
|
|
457
465
|
return Promise.resolve(this.childProviders.get(childId)!);
|
|
458
466
|
}
|
|
459
467
|
|
|
@@ -500,6 +508,10 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
500
508
|
childProvider.attach();
|
|
501
509
|
|
|
502
510
|
this.childProviders.set(childId, childProvider);
|
|
511
|
+
this.childAccessTimes.set(childId, Date.now());
|
|
512
|
+
|
|
513
|
+
// Evict least-recently-used children if over capacity
|
|
514
|
+
this.evictLRU();
|
|
503
515
|
|
|
504
516
|
this.emit("subdocLoaded", { childId, provider: childProvider });
|
|
505
517
|
|
|
@@ -511,6 +523,48 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
511
523
|
if (provider) {
|
|
512
524
|
provider.destroy();
|
|
513
525
|
this.childProviders.delete(childId);
|
|
526
|
+
this.childAccessTimes.delete(childId);
|
|
527
|
+
this.pinnedChildren.delete(childId);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Mark a child as pinned so LRU eviction will not remove it.
|
|
533
|
+
* Use this when a document is actively being viewed by the user.
|
|
534
|
+
*/
|
|
535
|
+
pinChild(childId: string) {
|
|
536
|
+
this.pinnedChildren.add(childId);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Unpin a child, allowing LRU eviction to reclaim it when capacity is exceeded.
|
|
541
|
+
*/
|
|
542
|
+
unpinChild(childId: string) {
|
|
543
|
+
this.pinnedChildren.delete(childId);
|
|
544
|
+
// Run eviction now in case we're over capacity
|
|
545
|
+
this.evictLRU();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Evict least-recently-used unpinned child providers until the cache is
|
|
550
|
+
* at or below MAX_CHILDREN.
|
|
551
|
+
*/
|
|
552
|
+
private evictLRU() {
|
|
553
|
+
if (this.childProviders.size <= AbracadabraProvider.MAX_CHILDREN) return;
|
|
554
|
+
|
|
555
|
+
// Build a list of evictable children sorted by last access (oldest first)
|
|
556
|
+
const evictable: Array<{ id: string; accessTime: number }> = [];
|
|
557
|
+
for (const [id] of this.childProviders) {
|
|
558
|
+
if (this.pinnedChildren.has(id)) continue;
|
|
559
|
+
evictable.push({ id, accessTime: this.childAccessTimes.get(id) ?? 0 });
|
|
560
|
+
}
|
|
561
|
+
evictable.sort((a, b) => a.accessTime - b.accessTime);
|
|
562
|
+
|
|
563
|
+
let toEvict = this.childProviders.size - AbracadabraProvider.MAX_CHILDREN;
|
|
564
|
+
for (const entry of evictable) {
|
|
565
|
+
if (toEvict <= 0) break;
|
|
566
|
+
this.unloadChild(entry.id);
|
|
567
|
+
toEvict--;
|
|
514
568
|
}
|
|
515
569
|
}
|
|
516
570
|
|
|
@@ -605,6 +659,8 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
605
659
|
provider.destroy();
|
|
606
660
|
}
|
|
607
661
|
this.childProviders.clear();
|
|
662
|
+
this.childAccessTimes.clear();
|
|
663
|
+
this.pinnedChildren.clear();
|
|
608
664
|
|
|
609
665
|
// Force-clear any stale providerMap entries for our children.
|
|
610
666
|
// detach() only removes if current === provider, so orphans can remain
|
|
@@ -328,13 +328,15 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
328
328
|
): Promise<boolean> {
|
|
329
329
|
if (this._destroyed) return true;
|
|
330
330
|
|
|
331
|
-
// Skip if already synced and doc hasn't changed since last sync
|
|
331
|
+
// Skip if already synced and doc hasn't changed since last sync.
|
|
332
|
+
// The 200ms grace margin handles edge cases where the updatedAt
|
|
333
|
+
// observer writes a timestamp milliseconds after lastSynced was recorded.
|
|
332
334
|
const existing = this.syncStates.get(docId);
|
|
333
335
|
if (
|
|
334
336
|
existing &&
|
|
335
337
|
existing.status === "synced" &&
|
|
336
338
|
existing.lastSynced !== null &&
|
|
337
|
-
existing.lastSynced >= updatedAt
|
|
339
|
+
existing.lastSynced + 200 >= updatedAt
|
|
338
340
|
) {
|
|
339
341
|
this.emit("stateChanged", { docId, state: existing });
|
|
340
342
|
return true;
|
|
@@ -391,6 +393,8 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
391
393
|
}
|
|
392
394
|
|
|
393
395
|
private async _syncNonE2EDoc(docId: string): Promise<DocSyncState> {
|
|
396
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree") as Y.Map<any>;
|
|
397
|
+
|
|
394
398
|
// Check if the provider already exists (user is viewing it) before loading.
|
|
395
399
|
const alreadyCached = this.rootProvider.children.has(docId);
|
|
396
400
|
|
|
@@ -415,39 +419,46 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
415
419
|
|
|
416
420
|
const childProvider = await this.rootProvider.loadChild(docId);
|
|
417
421
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
422
|
+
try {
|
|
423
|
+
// Wait for ready (offline snapshot loaded) then synced (server sync done)
|
|
424
|
+
await childProvider.ready;
|
|
425
|
+
await this._waitForSynced(childProvider);
|
|
421
426
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
}
|
|
427
|
+
// Emit docSynced while the child is still loaded so listeners can
|
|
428
|
+
// extract data (search text, file refs) without opening a new IDB.
|
|
429
|
+
{
|
|
430
|
+
const treeEntry = treeMap.get(docId);
|
|
431
|
+
this.emit("docSynced", {
|
|
432
|
+
docId,
|
|
433
|
+
document: childProvider.document,
|
|
434
|
+
label: treeEntry?.label ?? "",
|
|
435
|
+
meta: treeEntry?.meta,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
434
438
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
+
// Prefetch file blobs
|
|
440
|
+
if (this.opts.prefetchFiles && this.fileBlobStore) {
|
|
441
|
+
this._prefetchDocFiles(docId, childProvider.document).catch(() => null);
|
|
442
|
+
}
|
|
439
443
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
444
|
+
// Use the tree's updatedAt so lastSynced >= updatedAt, preventing
|
|
445
|
+
// the observer's timestamp write from triggering a spurious re-sync.
|
|
446
|
+
const treeEntry = treeMap.get(docId);
|
|
447
|
+
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
448
|
+
return { docId, status: "synced", lastSynced: Math.max(Date.now(), treeUpdatedAt), isE2E: false };
|
|
449
|
+
} finally {
|
|
450
|
+
// Always release the provider if it was created solely for background sync.
|
|
451
|
+
// This closes its IDB database, freeing the file descriptor.
|
|
452
|
+
// Providers that were already cached (user is viewing them) are kept alive.
|
|
453
|
+
if (!alreadyCached) {
|
|
454
|
+
this.rootProvider.unloadChild(docId);
|
|
455
|
+
}
|
|
445
456
|
}
|
|
446
|
-
|
|
447
|
-
return { docId, status: "synced", lastSynced: Date.now(), isE2E: false };
|
|
448
457
|
}
|
|
449
458
|
|
|
450
459
|
private async _syncE2EDoc(docId: string): Promise<DocSyncState> {
|
|
460
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree") as Y.Map<any>;
|
|
461
|
+
|
|
451
462
|
// Attempt E2E sync only when docKeyManager and keystore are available
|
|
452
463
|
const docKeyManager = (this.rootProvider as any).abracadabraConfig
|
|
453
464
|
?.docKeyManager;
|
|
@@ -474,7 +485,6 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
474
485
|
// Emit docSynced while the child is still alive so listeners can
|
|
475
486
|
// extract data without opening a new IDB connection.
|
|
476
487
|
{
|
|
477
|
-
const treeMap = this.rootProvider.document.getMap("doc-tree") as Y.Map<any>;
|
|
478
488
|
const treeEntry = treeMap.get(docId);
|
|
479
489
|
this.emit("docSynced", {
|
|
480
490
|
docId,
|
|
@@ -488,7 +498,11 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
488
498
|
this._prefetchDocFiles(docId, childDoc).catch(() => null);
|
|
489
499
|
}
|
|
490
500
|
|
|
491
|
-
|
|
501
|
+
// Use the tree's updatedAt so lastSynced >= updatedAt, preventing
|
|
502
|
+
// the observer's timestamp write from triggering a spurious re-sync.
|
|
503
|
+
const treeEntry = treeMap.get(docId);
|
|
504
|
+
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
505
|
+
return { docId, status: "synced", lastSynced: Math.max(Date.now(), treeUpdatedAt), isE2E: true };
|
|
492
506
|
} finally {
|
|
493
507
|
childProvider.destroy();
|
|
494
508
|
}
|