@abraca/dabra 0.6.0 → 0.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 +747 -66
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +725 -62
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +269 -31
- package/package.json +1 -2
- package/src/{HocuspocusProvider.ts → AbracadabraBaseProvider.ts} +33 -22
- package/src/AbracadabraClient.ts +96 -3
- package/src/AbracadabraProvider.ts +11 -11
- package/src/{HocuspocusProviderWebsocket.ts → AbracadabraWS.ts} +36 -22
- package/src/CloseEvents.ts +49 -0
- package/src/DocumentCache.ts +210 -0
- package/src/FileBlobStore.ts +300 -0
- package/src/MessageReceiver.ts +8 -8
- package/src/OutgoingMessages/AuthenticationMessage.ts +1 -1
- package/src/SearchIndex.ts +247 -0
- package/src/auth.ts +62 -0
- package/src/awarenessStatesToArray.ts +10 -0
- package/src/index.ts +9 -2
- package/src/types.ts +59 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndexedDB-backed cache for REST API metadata (documents, children, profiles,
|
|
3
|
+
* permissions, uploads). Provides offline-first read access to data fetched
|
|
4
|
+
* from the server.
|
|
5
|
+
*
|
|
6
|
+
* Each entry carries a `cachedAt` timestamp; reads return null once the TTL
|
|
7
|
+
* has elapsed. The cache is scoped by server origin so the same app can work
|
|
8
|
+
* against multiple backends without cross-contamination.
|
|
9
|
+
*
|
|
10
|
+
* Designed to be passed to AbracadabraClientConfig.cache so the client
|
|
11
|
+
* automatically checks the cache before hitting the network.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
DocumentMeta,
|
|
16
|
+
UserProfile,
|
|
17
|
+
PermissionEntry,
|
|
18
|
+
UploadInfo,
|
|
19
|
+
} from "./types.ts";
|
|
20
|
+
|
|
21
|
+
const DB_VERSION = 1;
|
|
22
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
23
|
+
|
|
24
|
+
function idbAvailable(): boolean {
|
|
25
|
+
return typeof globalThis !== "undefined" && "indexedDB" in globalThis;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function openDb(origin: string): Promise<IDBDatabase> {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const req = globalThis.indexedDB.open(
|
|
31
|
+
`abracadabra:meta-cache:${origin}`,
|
|
32
|
+
DB_VERSION,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
req.onupgradeneeded = (event) => {
|
|
36
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
37
|
+
for (const name of ["doc_meta", "children", "user_profile", "permissions", "uploads"]) {
|
|
38
|
+
if (!db.objectStoreNames.contains(name)) {
|
|
39
|
+
db.createObjectStore(name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
req.onsuccess = () => resolve(req.result);
|
|
45
|
+
req.onerror = () => reject(req.error);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function txPromise<T>(store: IDBObjectStore, request: IDBRequest<T>): Promise<T> {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
request.onsuccess = () => resolve(request.result);
|
|
52
|
+
request.onerror = () => reject(request.error);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DocumentCacheOptions {
|
|
57
|
+
/** How long cached entries remain valid. Default: 5 minutes. */
|
|
58
|
+
ttlMs?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class DocumentCache {
|
|
62
|
+
private readonly origin: string;
|
|
63
|
+
private readonly ttlMs: number;
|
|
64
|
+
private dbPromise: Promise<IDBDatabase | null> | null = null;
|
|
65
|
+
private db: IDBDatabase | null = null;
|
|
66
|
+
|
|
67
|
+
constructor(serverOrigin: string, opts?: DocumentCacheOptions) {
|
|
68
|
+
this.origin = serverOrigin;
|
|
69
|
+
this.ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private getDb(): Promise<IDBDatabase | null> {
|
|
73
|
+
if (!idbAvailable()) return Promise.resolve(null);
|
|
74
|
+
if (!this.dbPromise) {
|
|
75
|
+
this.dbPromise = openDb(this.origin)
|
|
76
|
+
.catch(() => null)
|
|
77
|
+
.then((db) => {
|
|
78
|
+
this.db = db;
|
|
79
|
+
return db;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return this.dbPromise;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private isExpired(cachedAt: number): boolean {
|
|
86
|
+
return Date.now() - cachedAt > this.ttlMs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Generic TTL helpers ───────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
private async getWithTtl<T>(
|
|
92
|
+
storeName: string,
|
|
93
|
+
key: string,
|
|
94
|
+
): Promise<T | null> {
|
|
95
|
+
const db = await this.getDb();
|
|
96
|
+
if (!db) return null;
|
|
97
|
+
const tx = db.transaction(storeName, "readonly");
|
|
98
|
+
const result = await txPromise<{ value: T; cachedAt: number } | undefined>(
|
|
99
|
+
tx.objectStore(storeName),
|
|
100
|
+
tx.objectStore(storeName).get(key),
|
|
101
|
+
);
|
|
102
|
+
if (!result) return null;
|
|
103
|
+
if (this.isExpired(result.cachedAt)) {
|
|
104
|
+
// Stale — evict asynchronously and signal miss
|
|
105
|
+
this.deleteKey(storeName, key).catch(() => null);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return result.value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async setWithTtl<T>(storeName: string, key: string, value: T): Promise<void> {
|
|
112
|
+
const db = await this.getDb();
|
|
113
|
+
if (!db) return;
|
|
114
|
+
const entry = { value, cachedAt: Date.now() };
|
|
115
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
116
|
+
await txPromise(
|
|
117
|
+
tx.objectStore(storeName),
|
|
118
|
+
tx.objectStore(storeName).put(entry, key),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async deleteKey(storeName: string, key: string): Promise<void> {
|
|
123
|
+
const db = await this.getDb();
|
|
124
|
+
if (!db) return;
|
|
125
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
126
|
+
await txPromise(tx.objectStore(storeName), tx.objectStore(storeName).delete(key));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Document metadata ─────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
async getDoc(docId: string): Promise<DocumentMeta | null> {
|
|
132
|
+
return this.getWithTtl<DocumentMeta>("doc_meta", docId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async setDoc(meta: DocumentMeta): Promise<void> {
|
|
136
|
+
return this.setWithTtl("doc_meta", meta.id, meta);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async invalidateDoc(docId: string): Promise<void> {
|
|
140
|
+
await this.deleteKey("doc_meta", docId).catch(() => null);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Children list ─────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
async getChildren(parentId: string): Promise<string[] | null> {
|
|
146
|
+
return this.getWithTtl<string[]>("children", parentId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async setChildren(parentId: string, items: string[]): Promise<void> {
|
|
150
|
+
return this.setWithTtl("children", parentId, items);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async invalidateChildren(parentId: string): Promise<void> {
|
|
154
|
+
await this.deleteKey("children", parentId).catch(() => null);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── User profile ──────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
async getProfile(userId: string): Promise<UserProfile | null> {
|
|
160
|
+
return this.getWithTtl<UserProfile>("user_profile", userId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async setProfile(profile: UserProfile): Promise<void> {
|
|
164
|
+
return this.setWithTtl("user_profile", profile.id, profile);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Get the cached profile for the currently authenticated user. */
|
|
168
|
+
async getCurrentProfile(): Promise<UserProfile | null> {
|
|
169
|
+
return this.getWithTtl<UserProfile>("user_profile", "__current__");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Cache a profile both by its ID and as the current user. */
|
|
173
|
+
async setCurrentProfile(profile: UserProfile): Promise<void> {
|
|
174
|
+
await Promise.all([
|
|
175
|
+
this.setWithTtl("user_profile", profile.id, profile),
|
|
176
|
+
this.setWithTtl("user_profile", "__current__", profile),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Permissions ───────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
async getPermissions(docId: string): Promise<PermissionEntry[] | null> {
|
|
183
|
+
return this.getWithTtl<PermissionEntry[]>("permissions", docId);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async setPermissions(docId: string, items: PermissionEntry[]): Promise<void> {
|
|
187
|
+
return this.setWithTtl("permissions", docId, items);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Uploads list ──────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
async getUploads(docId: string): Promise<UploadInfo[] | null> {
|
|
193
|
+
return this.getWithTtl<UploadInfo[]>("uploads", docId);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async setUploads(docId: string, items: UploadInfo[]): Promise<void> {
|
|
197
|
+
return this.setWithTtl("uploads", docId, items);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async invalidateUploads(docId: string): Promise<void> {
|
|
201
|
+
await this.deleteKey("uploads", docId).catch(() => null);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
destroy(): void {
|
|
207
|
+
this.db?.close();
|
|
208
|
+
this.db = null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndexedDB-backed file blob cache with an offline-tolerant upload queue.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Cache downloaded file blobs locally so they can be served offline via
|
|
6
|
+
* object URLs (URL.createObjectURL).
|
|
7
|
+
* - Queue file uploads when the network is unavailable. The queue persists
|
|
8
|
+
* across page reloads (IndexedDB-backed).
|
|
9
|
+
* - Auto-flush the upload queue when the browser reports it is back online,
|
|
10
|
+
* and expose flushQueue() for manual flushing.
|
|
11
|
+
*
|
|
12
|
+
* Events:
|
|
13
|
+
* - "upload:queued" — entry added to queue
|
|
14
|
+
* - "upload:started" — upload attempt started
|
|
15
|
+
* - "upload:done" — upload succeeded
|
|
16
|
+
* - "upload:error" — upload attempt failed
|
|
17
|
+
*
|
|
18
|
+
* Falls back to a silent no-op when IndexedDB is unavailable (SSR / Node.js).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { UploadQueueEntry } from "./types.ts";
|
|
22
|
+
import type { AbracadabraClient } from "./AbracadabraClient.ts";
|
|
23
|
+
import EventEmitter from "./EventEmitter.ts";
|
|
24
|
+
|
|
25
|
+
const DB_VERSION = 1;
|
|
26
|
+
|
|
27
|
+
function idbAvailable(): boolean {
|
|
28
|
+
return typeof globalThis !== "undefined" && "indexedDB" in globalThis;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function openDb(origin: string): Promise<IDBDatabase> {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const req = globalThis.indexedDB.open(`abracadabra:files:${origin}`, DB_VERSION);
|
|
34
|
+
|
|
35
|
+
req.onupgradeneeded = (event) => {
|
|
36
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
37
|
+
if (!db.objectStoreNames.contains("blobs")) {
|
|
38
|
+
db.createObjectStore("blobs");
|
|
39
|
+
}
|
|
40
|
+
if (!db.objectStoreNames.contains("upload_queue")) {
|
|
41
|
+
db.createObjectStore("upload_queue", { keyPath: "id" });
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
req.onsuccess = () => resolve(req.result);
|
|
46
|
+
req.onerror = () => reject(req.error);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function txPromise<T>(store: IDBObjectStore, request: IDBRequest<T>): Promise<T> {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
request.onsuccess = () => resolve(request.result);
|
|
53
|
+
request.onerror = () => reject(request.error);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface BlobCacheEntry {
|
|
58
|
+
blob: Blob;
|
|
59
|
+
mime_type: string;
|
|
60
|
+
filename: string;
|
|
61
|
+
cachedAt: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class FileBlobStore extends EventEmitter {
|
|
65
|
+
private readonly origin: string;
|
|
66
|
+
private readonly client: AbracadabraClient;
|
|
67
|
+
private dbPromise: Promise<IDBDatabase | null> | null = null;
|
|
68
|
+
private db: IDBDatabase | null = null;
|
|
69
|
+
|
|
70
|
+
/** Tracks active object URLs so we can revoke them on destroy. */
|
|
71
|
+
private readonly objectUrls = new Map<string, string>();
|
|
72
|
+
|
|
73
|
+
/** Prevents concurrent flush runs. */
|
|
74
|
+
private _flushing = false;
|
|
75
|
+
|
|
76
|
+
private readonly _onlineHandler: () => void;
|
|
77
|
+
|
|
78
|
+
constructor(serverOrigin: string, client: AbracadabraClient) {
|
|
79
|
+
super();
|
|
80
|
+
this.origin = serverOrigin;
|
|
81
|
+
this.client = client;
|
|
82
|
+
|
|
83
|
+
this._onlineHandler = () => { this.flushQueue().catch(() => null); };
|
|
84
|
+
if (typeof window !== "undefined") {
|
|
85
|
+
window.addEventListener("online", this._onlineHandler);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private getDb(): Promise<IDBDatabase | null> {
|
|
90
|
+
if (!idbAvailable()) return Promise.resolve(null);
|
|
91
|
+
if (!this.dbPromise) {
|
|
92
|
+
this.dbPromise = openDb(this.origin)
|
|
93
|
+
.catch(() => null)
|
|
94
|
+
.then((db) => {
|
|
95
|
+
this.db = db;
|
|
96
|
+
return db;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return this.dbPromise;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private blobKey(docId: string, uploadId: string): string {
|
|
103
|
+
return `${docId}/${uploadId}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Blob cache ────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Return a local object URL for a file.
|
|
110
|
+
* On first call the blob is downloaded from the server and cached in IDB.
|
|
111
|
+
* Returns null when offline and the blob is not yet cached, or when
|
|
112
|
+
* URL.createObjectURL is unavailable (e.g. Node.js / SSR).
|
|
113
|
+
*/
|
|
114
|
+
async getBlobUrl(docId: string, uploadId: string): Promise<string | null> {
|
|
115
|
+
// Object URLs are only meaningful in browser environments.
|
|
116
|
+
if (typeof window === "undefined") return null;
|
|
117
|
+
|
|
118
|
+
const key = this.blobKey(docId, uploadId);
|
|
119
|
+
|
|
120
|
+
// Reuse existing in-memory object URL if available
|
|
121
|
+
const existing = this.objectUrls.get(key);
|
|
122
|
+
if (existing) return existing;
|
|
123
|
+
|
|
124
|
+
const db = await this.getDb();
|
|
125
|
+
if (db) {
|
|
126
|
+
const tx = db.transaction("blobs", "readonly");
|
|
127
|
+
const entry = await txPromise<BlobCacheEntry | undefined>(
|
|
128
|
+
tx.objectStore("blobs"),
|
|
129
|
+
tx.objectStore("blobs").get(key),
|
|
130
|
+
);
|
|
131
|
+
if (entry) {
|
|
132
|
+
const url = URL.createObjectURL(entry.blob);
|
|
133
|
+
this.objectUrls.set(key, url);
|
|
134
|
+
return url;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Not cached — try downloading
|
|
139
|
+
let blob: Blob;
|
|
140
|
+
try {
|
|
141
|
+
blob = await this.client.getUpload(docId, uploadId);
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Cache the blob
|
|
147
|
+
if (db) {
|
|
148
|
+
const entry: BlobCacheEntry = {
|
|
149
|
+
blob,
|
|
150
|
+
mime_type: blob.type,
|
|
151
|
+
filename: uploadId,
|
|
152
|
+
cachedAt: Date.now(),
|
|
153
|
+
};
|
|
154
|
+
const tx = db.transaction("blobs", "readwrite");
|
|
155
|
+
tx.objectStore("blobs").put(entry, key);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const url = URL.createObjectURL(blob);
|
|
159
|
+
this.objectUrls.set(key, url);
|
|
160
|
+
return url;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Revoke the object URL and remove the blob from cache. */
|
|
164
|
+
async evictBlob(docId: string, uploadId: string): Promise<void> {
|
|
165
|
+
const key = this.blobKey(docId, uploadId);
|
|
166
|
+
|
|
167
|
+
const url = this.objectUrls.get(key);
|
|
168
|
+
if (url) {
|
|
169
|
+
URL.revokeObjectURL(url);
|
|
170
|
+
this.objectUrls.delete(key);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const db = await this.getDb();
|
|
174
|
+
if (!db) return;
|
|
175
|
+
const tx = db.transaction("blobs", "readwrite");
|
|
176
|
+
await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").delete(key));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Upload queue ──────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Queue a file for upload. Works offline — the entry is persisted to IDB
|
|
183
|
+
* and flushed the next time the queue is flushed.
|
|
184
|
+
* Returns the generated queue entry id.
|
|
185
|
+
*/
|
|
186
|
+
async queueUpload(
|
|
187
|
+
docId: string,
|
|
188
|
+
file: File | Blob,
|
|
189
|
+
filename?: string,
|
|
190
|
+
): Promise<string> {
|
|
191
|
+
const id = crypto.randomUUID();
|
|
192
|
+
const resolvedFilename =
|
|
193
|
+
file instanceof File ? file.name : (filename ?? "file");
|
|
194
|
+
|
|
195
|
+
const entry: UploadQueueEntry = {
|
|
196
|
+
id,
|
|
197
|
+
docId,
|
|
198
|
+
file,
|
|
199
|
+
filename: resolvedFilename,
|
|
200
|
+
status: "pending",
|
|
201
|
+
createdAt: Date.now(),
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const db = await this.getDb();
|
|
205
|
+
if (db) {
|
|
206
|
+
const tx = db.transaction("upload_queue", "readwrite");
|
|
207
|
+
await txPromise(
|
|
208
|
+
tx.objectStore("upload_queue"),
|
|
209
|
+
tx.objectStore("upload_queue").put(entry),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.emit("upload:queued", entry);
|
|
214
|
+
return id;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Return all upload queue entries. */
|
|
218
|
+
async getQueue(): Promise<UploadQueueEntry[]> {
|
|
219
|
+
const db = await this.getDb();
|
|
220
|
+
if (!db) return [];
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const tx = db.transaction("upload_queue", "readonly");
|
|
223
|
+
const req = tx.objectStore("upload_queue").getAll();
|
|
224
|
+
req.onsuccess = () => resolve(req.result as UploadQueueEntry[]);
|
|
225
|
+
req.onerror = () => reject(req.error);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Upload all pending queue entries via AbracadabraClient.
|
|
231
|
+
* Safe to call repeatedly — a concurrent call is a no-op.
|
|
232
|
+
* Entries that fail are marked with status "error" and left in the queue.
|
|
233
|
+
*/
|
|
234
|
+
async flushQueue(): Promise<void> {
|
|
235
|
+
if (this._flushing) return;
|
|
236
|
+
this._flushing = true;
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const all = await this.getQueue();
|
|
240
|
+
const pending = all.filter((e) => e.status === "pending");
|
|
241
|
+
|
|
242
|
+
for (const entry of pending) {
|
|
243
|
+
await this._updateQueueEntry(entry.id, { status: "uploading" });
|
|
244
|
+
this.emit("upload:started", { ...entry, status: "uploading" });
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
await this.client.upload(entry.docId, entry.file, entry.filename);
|
|
248
|
+
await this._updateQueueEntry(entry.id, { status: "done" });
|
|
249
|
+
this.emit("upload:done", { ...entry, status: "done" });
|
|
250
|
+
} catch (err) {
|
|
251
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
252
|
+
await this._updateQueueEntry(entry.id, { status: "error", error: message });
|
|
253
|
+
this.emit("upload:error", { ...entry, status: "error", error: message });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} finally {
|
|
257
|
+
this._flushing = false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async _updateQueueEntry(
|
|
262
|
+
id: string,
|
|
263
|
+
patch: Partial<UploadQueueEntry>,
|
|
264
|
+
): Promise<void> {
|
|
265
|
+
const db = await this.getDb();
|
|
266
|
+
if (!db) return;
|
|
267
|
+
|
|
268
|
+
return new Promise<void>((resolve, reject) => {
|
|
269
|
+
const tx = db.transaction("upload_queue", "readwrite");
|
|
270
|
+
const store = tx.objectStore("upload_queue");
|
|
271
|
+
const req = store.get(id);
|
|
272
|
+
req.onsuccess = () => {
|
|
273
|
+
if (!req.result) { resolve(); return; }
|
|
274
|
+
const updated = { ...req.result, ...patch };
|
|
275
|
+
store.put(updated);
|
|
276
|
+
tx.oncomplete = () => resolve();
|
|
277
|
+
tx.onerror = () => reject(tx.error);
|
|
278
|
+
};
|
|
279
|
+
req.onerror = () => reject(req.error);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
destroy(): void {
|
|
286
|
+
if (typeof window !== "undefined") {
|
|
287
|
+
window.removeEventListener("online", this._onlineHandler);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Revoke all tracked object URLs
|
|
291
|
+
for (const url of this.objectUrls.values()) {
|
|
292
|
+
URL.revokeObjectURL(url);
|
|
293
|
+
}
|
|
294
|
+
this.objectUrls.clear();
|
|
295
|
+
|
|
296
|
+
this.db?.close();
|
|
297
|
+
this.db = null;
|
|
298
|
+
this.removeAllListeners();
|
|
299
|
+
}
|
|
300
|
+
}
|
package/src/MessageReceiver.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { readAuthMessage } from "
|
|
1
|
+
import { readAuthMessage } from "./auth.ts";
|
|
2
2
|
import { readVarInt, readVarString } from "lib0/decoding";
|
|
3
3
|
import type { CloseEvent } from "ws";
|
|
4
4
|
import * as awarenessProtocol from "y-protocols/awareness";
|
|
5
5
|
import { messageYjsSyncStep2, readSyncMessage } from "y-protocols/sync";
|
|
6
|
-
import type {
|
|
6
|
+
import type { AbracadabraBaseProvider } from "./AbracadabraBaseProvider.ts";
|
|
7
7
|
import type { IncomingMessage } from "./IncomingMessage.ts";
|
|
8
8
|
import { OutgoingMessage } from "./OutgoingMessage.ts";
|
|
9
9
|
import { MessageType } from "./types.ts";
|
|
@@ -15,7 +15,7 @@ export class MessageReceiver {
|
|
|
15
15
|
this.message = message;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
public apply(provider:
|
|
18
|
+
public apply(provider: AbracadabraBaseProvider, emitSynced: boolean) {
|
|
19
19
|
const { message } = this;
|
|
20
20
|
const type = message.readVarUint();
|
|
21
21
|
|
|
@@ -72,7 +72,7 @@ export class MessageReceiver {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
private applySyncMessage(provider:
|
|
75
|
+
private applySyncMessage(provider: AbracadabraBaseProvider, emitSynced: boolean) {
|
|
76
76
|
const { message } = this;
|
|
77
77
|
|
|
78
78
|
message.writeVarUint(MessageType.Sync);
|
|
@@ -91,13 +91,13 @@ export class MessageReceiver {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
applySyncStatusMessage(provider:
|
|
94
|
+
applySyncStatusMessage(provider: AbracadabraBaseProvider, applied: boolean) {
|
|
95
95
|
if (applied) {
|
|
96
96
|
provider.decrementUnsyncedChanges();
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
private applyAwarenessMessage(provider:
|
|
100
|
+
private applyAwarenessMessage(provider: AbracadabraBaseProvider) {
|
|
101
101
|
if (!provider.awareness) return;
|
|
102
102
|
|
|
103
103
|
const { message } = this;
|
|
@@ -109,7 +109,7 @@ export class MessageReceiver {
|
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
private applyAuthMessage(provider:
|
|
112
|
+
private applyAuthMessage(provider: AbracadabraBaseProvider) {
|
|
113
113
|
const { message } = this;
|
|
114
114
|
|
|
115
115
|
readAuthMessage(
|
|
@@ -120,7 +120,7 @@ export class MessageReceiver {
|
|
|
120
120
|
);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
private applyQueryAwarenessMessage(provider:
|
|
123
|
+
private applyQueryAwarenessMessage(provider: AbracadabraBaseProvider) {
|
|
124
124
|
if (!provider.awareness) return;
|
|
125
125
|
|
|
126
126
|
const { message } = this;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeVarString, writeVarUint } from "lib0/encoding";
|
|
2
|
-
import { writeAuthentication } from "
|
|
2
|
+
import { writeAuthentication } from "../auth.ts";
|
|
3
3
|
import type { OutgoingMessageArguments } from "../types.ts";
|
|
4
4
|
import { MessageType } from "../types.ts";
|
|
5
5
|
import { OutgoingMessage } from "../OutgoingMessage.ts";
|