@rgby/collab-core 1.0.1
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/README.md +60 -0
- package/dist/collab-provider.d.ts +561 -0
- package/dist/collab-provider.d.ts.map +1 -0
- package/dist/collab-provider.js +1389 -0
- package/dist/collab-provider.js.map +1 -0
- package/dist/extensions/collaboration-cursor.d.ts +87 -0
- package/dist/extensions/collaboration-cursor.d.ts.map +1 -0
- package/dist/extensions/collaboration-cursor.js +82 -0
- package/dist/extensions/collaboration-cursor.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/services/sw-manager.d.ts +62 -0
- package/dist/services/sw-manager.d.ts.map +1 -0
- package/dist/services/sw-manager.js +207 -0
- package/dist/services/sw-manager.js.map +1 -0
- package/dist/test-deps.d.ts +15 -0
- package/dist/test-deps.d.ts.map +1 -0
- package/dist/test-deps.js +21 -0
- package/dist/test-deps.js.map +1 -0
- package/dist/upload-queue.d.ts +82 -0
- package/dist/upload-queue.d.ts.map +1 -0
- package/dist/upload-queue.js +273 -0
- package/dist/upload-queue.js.map +1 -0
- package/dist/useCollab.d.ts +216 -0
- package/dist/useCollab.d.ts.map +1 -0
- package/dist/useCollab.js +583 -0
- package/dist/useCollab.js.map +1 -0
- package/package.json +93 -0
- package/src/collab-provider.ts +1871 -0
- package/src/extensions/collaboration-cursor.ts +188 -0
- package/src/index.ts +6 -0
- package/src/services/sw-manager.ts +255 -0
- package/src/test-deps.ts +34 -0
- package/src/upload-queue.ts +337 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload Queue Manager
|
|
3
|
+
* Handles offline file uploads with IndexedDB queue
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PendingUpload {
|
|
7
|
+
id: string;
|
|
8
|
+
spaceId: string;
|
|
9
|
+
documentId: string | null;
|
|
10
|
+
file: Blob;
|
|
11
|
+
filename: string;
|
|
12
|
+
mimeType: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
retryCount: number;
|
|
15
|
+
tempUrl: string; // blob: URL for immediate display
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UploadQueueOptions {
|
|
19
|
+
dbName?: string;
|
|
20
|
+
storeName?: string;
|
|
21
|
+
maxRetries?: number;
|
|
22
|
+
retryDelay?: number; // Base delay in ms, exponential backoff
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type UploadQueueEventType = 'queue-changed' | 'upload-complete' | 'upload-failed';
|
|
26
|
+
|
|
27
|
+
export interface UploadQueueEvent {
|
|
28
|
+
type: UploadQueueEventType;
|
|
29
|
+
upload?: PendingUpload;
|
|
30
|
+
error?: Error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class UploadQueueManager {
|
|
34
|
+
private db: IDBDatabase | null = null;
|
|
35
|
+
private dbName: string;
|
|
36
|
+
private storeName: string;
|
|
37
|
+
private maxRetries: number;
|
|
38
|
+
private retryDelay: number;
|
|
39
|
+
private listeners: Map<UploadQueueEventType, Set<(event: UploadQueueEvent) => void>> = new Map();
|
|
40
|
+
private isProcessing: boolean = false;
|
|
41
|
+
|
|
42
|
+
constructor(options: UploadQueueOptions = {}) {
|
|
43
|
+
this.dbName = options.dbName || 'collab-upload-queue';
|
|
44
|
+
this.storeName = options.storeName || 'pending_uploads';
|
|
45
|
+
this.maxRetries = options.maxRetries || 3;
|
|
46
|
+
this.retryDelay = options.retryDelay || 1000;
|
|
47
|
+
|
|
48
|
+
// Listen for online/offline events
|
|
49
|
+
window.addEventListener('online', () => {
|
|
50
|
+
console.log('[UploadQueue] Network online, processing queue');
|
|
51
|
+
this.processQueue();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
window.addEventListener('offline', () => {
|
|
55
|
+
console.log('[UploadQueue] Network offline');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initialize the IndexedDB database
|
|
61
|
+
*/
|
|
62
|
+
async init(): Promise<void> {
|
|
63
|
+
if (this.db) return;
|
|
64
|
+
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
67
|
+
|
|
68
|
+
request.onerror = () => {
|
|
69
|
+
reject(new Error('Failed to open IndexedDB'));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
request.onsuccess = () => {
|
|
73
|
+
this.db = request.result;
|
|
74
|
+
console.log('[UploadQueue] IndexedDB initialized');
|
|
75
|
+
resolve();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
request.onupgradeneeded = (event) => {
|
|
79
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
80
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
81
|
+
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
|
|
82
|
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
83
|
+
store.createIndex('spaceId', 'spaceId', { unique: false });
|
|
84
|
+
console.log('[UploadQueue] Object store created');
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Add an upload to the queue
|
|
92
|
+
*/
|
|
93
|
+
async add(upload: Omit<PendingUpload, 'id' | 'timestamp' | 'retryCount' | 'tempUrl'>): Promise<PendingUpload> {
|
|
94
|
+
await this.init();
|
|
95
|
+
if (!this.db) throw new Error('Database not initialized');
|
|
96
|
+
|
|
97
|
+
const id = `upload-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
98
|
+
const tempUrl = URL.createObjectURL(upload.file);
|
|
99
|
+
|
|
100
|
+
const pendingUpload: PendingUpload = {
|
|
101
|
+
id,
|
|
102
|
+
spaceId: upload.spaceId,
|
|
103
|
+
documentId: upload.documentId,
|
|
104
|
+
file: upload.file,
|
|
105
|
+
filename: upload.filename,
|
|
106
|
+
mimeType: upload.mimeType,
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
retryCount: 0,
|
|
109
|
+
tempUrl
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const transaction = this.db!.transaction([this.storeName], 'readwrite');
|
|
114
|
+
const store = transaction.objectStore(this.storeName);
|
|
115
|
+
const request = store.add(pendingUpload);
|
|
116
|
+
|
|
117
|
+
request.onsuccess = () => {
|
|
118
|
+
console.log('[UploadQueue] Upload added to queue:', id);
|
|
119
|
+
this.emit('queue-changed', { type: 'queue-changed', upload: pendingUpload });
|
|
120
|
+
resolve(pendingUpload);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
request.onerror = () => {
|
|
124
|
+
reject(new Error('Failed to add upload to queue'));
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get all pending uploads
|
|
131
|
+
*/
|
|
132
|
+
async getAll(): Promise<PendingUpload[]> {
|
|
133
|
+
await this.init();
|
|
134
|
+
if (!this.db) return [];
|
|
135
|
+
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const transaction = this.db!.transaction([this.storeName], 'readonly');
|
|
138
|
+
const store = transaction.objectStore(this.storeName);
|
|
139
|
+
const request = store.getAll();
|
|
140
|
+
|
|
141
|
+
request.onsuccess = () => {
|
|
142
|
+
resolve(request.result || []);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
request.onerror = () => {
|
|
146
|
+
reject(new Error('Failed to get uploads from queue'));
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Remove an upload from the queue
|
|
153
|
+
*/
|
|
154
|
+
async remove(id: string): Promise<void> {
|
|
155
|
+
await this.init();
|
|
156
|
+
if (!this.db) return;
|
|
157
|
+
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const transaction = this.db!.transaction([this.storeName], 'readwrite');
|
|
160
|
+
const store = transaction.objectStore(this.storeName);
|
|
161
|
+
const request = store.delete(id);
|
|
162
|
+
|
|
163
|
+
request.onsuccess = () => {
|
|
164
|
+
console.log('[UploadQueue] Upload removed from queue:', id);
|
|
165
|
+
this.emit('queue-changed', { type: 'queue-changed' });
|
|
166
|
+
resolve();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
request.onerror = () => {
|
|
170
|
+
reject(new Error('Failed to remove upload from queue'));
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Update an upload in the queue
|
|
177
|
+
*/
|
|
178
|
+
async update(upload: PendingUpload): Promise<void> {
|
|
179
|
+
await this.init();
|
|
180
|
+
if (!this.db) return;
|
|
181
|
+
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
const transaction = this.db!.transaction([this.storeName], 'readwrite');
|
|
184
|
+
const store = transaction.objectStore(this.storeName);
|
|
185
|
+
const request = store.put(upload);
|
|
186
|
+
|
|
187
|
+
request.onsuccess = () => {
|
|
188
|
+
resolve();
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
request.onerror = () => {
|
|
192
|
+
reject(new Error('Failed to update upload in queue'));
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Clear all uploads from the queue
|
|
199
|
+
*/
|
|
200
|
+
async clear(): Promise<void> {
|
|
201
|
+
await this.init();
|
|
202
|
+
if (!this.db) return;
|
|
203
|
+
|
|
204
|
+
// Revoke all blob URLs first
|
|
205
|
+
const uploads = await this.getAll();
|
|
206
|
+
uploads.forEach(upload => {
|
|
207
|
+
if (upload.tempUrl.startsWith('blob:')) {
|
|
208
|
+
URL.revokeObjectURL(upload.tempUrl);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const transaction = this.db!.transaction([this.storeName], 'readwrite');
|
|
214
|
+
const store = transaction.objectStore(this.storeName);
|
|
215
|
+
const request = store.clear();
|
|
216
|
+
|
|
217
|
+
request.onsuccess = () => {
|
|
218
|
+
console.log('[UploadQueue] Queue cleared');
|
|
219
|
+
this.emit('queue-changed', { type: 'queue-changed' });
|
|
220
|
+
resolve();
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
request.onerror = () => {
|
|
224
|
+
reject(new Error('Failed to clear queue'));
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Process the queue - upload pending files
|
|
231
|
+
*/
|
|
232
|
+
async processQueue(uploadFn?: (upload: PendingUpload) => Promise<any>): Promise<void> {
|
|
233
|
+
if (this.isProcessing || !navigator.onLine) {
|
|
234
|
+
console.log('[UploadQueue] Skipping queue processing (processing:', this.isProcessing, 'online:', navigator.onLine, ')');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.isProcessing = true;
|
|
239
|
+
console.log('[UploadQueue] Processing queue...');
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const uploads = await this.getAll();
|
|
243
|
+
console.log('[UploadQueue] Found', uploads.length, 'pending uploads');
|
|
244
|
+
|
|
245
|
+
for (const upload of uploads) {
|
|
246
|
+
try {
|
|
247
|
+
if (!uploadFn) {
|
|
248
|
+
console.warn('[UploadQueue] No upload function provided, skipping upload:', upload.id);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log('[UploadQueue] Processing upload:', upload.id);
|
|
253
|
+
const result = await uploadFn(upload);
|
|
254
|
+
|
|
255
|
+
// Success - remove from queue and revoke blob URL
|
|
256
|
+
await this.remove(upload.id);
|
|
257
|
+
if (upload.tempUrl.startsWith('blob:')) {
|
|
258
|
+
URL.revokeObjectURL(upload.tempUrl);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.emit('upload-complete', { type: 'upload-complete', upload });
|
|
262
|
+
console.log('[UploadQueue] Upload complete:', upload.id);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error('[UploadQueue] Upload failed:', upload.id, error);
|
|
265
|
+
|
|
266
|
+
// Increment retry count
|
|
267
|
+
upload.retryCount += 1;
|
|
268
|
+
|
|
269
|
+
if (upload.retryCount >= this.maxRetries) {
|
|
270
|
+
// Max retries reached - remove from queue
|
|
271
|
+
console.error('[UploadQueue] Max retries reached, removing:', upload.id);
|
|
272
|
+
await this.remove(upload.id);
|
|
273
|
+
if (upload.tempUrl.startsWith('blob:')) {
|
|
274
|
+
URL.revokeObjectURL(upload.tempUrl);
|
|
275
|
+
}
|
|
276
|
+
this.emit('upload-failed', { type: 'upload-failed', upload, error: error as Error });
|
|
277
|
+
} else {
|
|
278
|
+
// Update retry count and schedule retry
|
|
279
|
+
await this.update(upload);
|
|
280
|
+
console.log('[UploadQueue] Retry scheduled for:', upload.id, 'attempt:', upload.retryCount);
|
|
281
|
+
|
|
282
|
+
// Exponential backoff
|
|
283
|
+
const delay = this.retryDelay * Math.pow(2, upload.retryCount - 1);
|
|
284
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} finally {
|
|
289
|
+
this.isProcessing = false;
|
|
290
|
+
console.log('[UploadQueue] Queue processing complete');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get the number of pending uploads
|
|
296
|
+
*/
|
|
297
|
+
async count(): Promise<number> {
|
|
298
|
+
const uploads = await this.getAll();
|
|
299
|
+
return uploads.length;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Add event listener
|
|
304
|
+
*/
|
|
305
|
+
on(event: UploadQueueEventType, callback: (event: UploadQueueEvent) => void): () => void {
|
|
306
|
+
if (!this.listeners.has(event)) {
|
|
307
|
+
this.listeners.set(event, new Set());
|
|
308
|
+
}
|
|
309
|
+
this.listeners.get(event)!.add(callback);
|
|
310
|
+
|
|
311
|
+
// Return unsubscribe function
|
|
312
|
+
return () => {
|
|
313
|
+
this.listeners.get(event)?.delete(callback);
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Emit event
|
|
319
|
+
*/
|
|
320
|
+
private emit(event: UploadQueueEventType, data: UploadQueueEvent): void {
|
|
321
|
+
const callbacks = this.listeners.get(event);
|
|
322
|
+
if (callbacks) {
|
|
323
|
+
callbacks.forEach(callback => callback(data));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Close the database connection
|
|
329
|
+
*/
|
|
330
|
+
async close(): Promise<void> {
|
|
331
|
+
if (this.db) {
|
|
332
|
+
this.db.close();
|
|
333
|
+
this.db = null;
|
|
334
|
+
console.log('[UploadQueue] Database closed');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|