Skip to content

Commit ff25b2b

Browse files
author
Amine
committed
tests: fixed SyncService to rely on LocalStorage and be agnostic to file saving.
1 parent 704c2a8 commit ff25b2b

File tree

8 files changed

+43
-64
lines changed

8 files changed

+43
-64
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ dist
99
# Useful if running repository in VSCode dev container
1010
.pnpm-store
1111
__screenshots__
12-
**/tests/temp/**/*
13-
**/tests/temp/**/.*
12+
testing.db
13+
testing.db-*

packages/attachments/src/AttachmentContext.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -117,20 +117,9 @@ export class AttachmentContext {
117117
* @param attachment - The attachment record to upsert
118118
* @param context - Active database transaction context
119119
*/
120-
upsertAttachment(attachment: AttachmentRecord, context: Transaction): void {
121-
console.debug('[ATTACHMENT CONTEXT] Upserting attachment:', [
122-
attachment.id,
123-
attachment.filename,
124-
attachment.localUri || null,
125-
attachment.size || null,
126-
attachment.mediaType || null,
127-
attachment.timestamp,
128-
attachment.state,
129-
attachment.hasSynced ? 1 : 0,
130-
attachment.metaData || null
131-
]);
120+
async upsertAttachment(attachment: AttachmentRecord, context: Transaction): Promise<void> {
132121
try {
133-
context.execute(
122+
const result = await context.execute(
134123
/* sql */
135124
`
136125
INSERT
@@ -161,7 +150,6 @@ export class AttachmentContext {
161150
]
162151
);
163152
} catch (error) {
164-
console.error('[ATTACHMENT CONTEXT] Error upserting attachment:', attachment.id?.substring(0,8), attachment.state, error);
165153
throw error;
166154
}
167155
}
@@ -198,9 +186,7 @@ export class AttachmentContext {
198186
* @param attachments - Array of attachment records to save
199187
*/
200188
async saveAttachments(attachments: AttachmentRecord[]): Promise<void> {
201-
console.debug('[ATTACHMENT CONTEXT] Saving attachments:', attachments.map(a => ({ id: a.id?.substring(0,8), state: a.state })));
202189
if (attachments.length === 0) {
203-
console.debug('[ATTACHMENT CONTEXT] No attachments to save');
204190
return;
205191
}
206192
await this.db.writeTransaction(async (tx) => {

packages/attachments/src/AttachmentQueue.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { AbstractPowerSyncDatabase, DifferentialWatchedQuery, ILogger, Transaction } from '@powersync/common';
22
import { AttachmentContext } from './AttachmentContext.js';
3-
import { LocalStorageAdapter } from './LocalStorageAdapter.js';
3+
import { AttachmentData, LocalStorageAdapter } from './LocalStorageAdapter.js';
44
import { RemoteStorageAdapter } from './RemoteStorageAdapter.js';
55
import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js';
66
import { SyncingService } from './SyncingService.js';
77
import { WatchedAttachmentItem } from './WatchedAttachmentItem.js';
88
import { AttachmentService } from './AttachmentService.js';
99

10-
export type AttachmentData = ArrayBuffer | Blob | string;
11-
1210
/**
1311
* AttachmentQueue manages the lifecycle and synchronization of attachments
1412
* between local and remote storage.
@@ -96,7 +94,6 @@ export class AttachmentQueue {
9694
downloadAttachments?: boolean;
9795
archivedCacheLimit?: number;
9896
}) {
99-
console.debug('AttachmentQueue constructor')
10097
this.context = new AttachmentContext(db, tableName, logger ?? db.logger);
10198
this.remoteStorage = remoteStorage;
10299
this.localStorage = localStorage;
@@ -136,7 +133,6 @@ export class AttachmentQueue {
136133
* - Handles state transitions for archived and new attachments
137134
*/
138135
async startSync(): Promise<void> {
139-
console.debug('[QUEUE] AttachmentQueue startSync')
140136
if (this.attachmentService.watchActiveAttachments) {
141137
await this.stopSync();
142138
// re-create the watch after it was stopped
@@ -151,14 +147,12 @@ export class AttachmentQueue {
151147
// Sync storage when there is a change in active attachments
152148
this.watchActiveAttachments.registerListener({
153149
onDiff: async () => {
154-
console.debug('[QUEUE] watchActiveAttachments: diff detected, syncing storage');
155150
await this.syncStorage();
156151
}
157152
});
158153

159154
// Process attachments when there is a change in watched attachments
160155
this.watchAttachments(async (watchedAttachments) => {
161-
console.debug('[QUEUE] watchAttachments callback:', watchedAttachments.length, 'items');
162156
// Need to get all the attachments which are tracked in the DB.
163157
// We might need to restore an archived attachment.
164158
const currentAttachments = await this.context.getAttachments();
@@ -234,7 +228,6 @@ export class AttachmentQueue {
234228
}
235229

236230
if (attachmentUpdates.length > 0) {
237-
console.debug('[QUEUE] Saving attachments:', attachmentUpdates);
238231
await this.context.saveAttachments(attachmentUpdates);
239232
}
240233
});
@@ -248,7 +241,6 @@ export class AttachmentQueue {
248241
*/
249242
async syncStorage(): Promise<void> {
250243
const activeAttachments = await this.context.getActiveAttachments();
251-
console.debug('[QUEUE] syncStorage: processing', activeAttachments.length, 'active attachments');
252244
await this.localStorage.initialize();
253245
await this.syncingService.processAttachments(activeAttachments);
254246
await this.syncingService.deleteArchivedAttachments();

packages/attachments/src/LocalStorageAdapter.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
export type AttachmentData = ArrayBuffer | string;
2+
13
export enum EncodingType {
24
UTF8 = 'utf8',
35
Base64 = 'base64'
46
}
57

8+
69
/**
710
* LocalStorageAdapter defines the interface for local file storage operations.
811
* Implementations handle file I/O, directory management, and storage initialization.
@@ -14,7 +17,7 @@ export interface LocalStorageAdapter {
1417
* @param data Data to store (ArrayBuffer, Blob, or string)
1518
* @returns Number of bytes written
1619
*/
17-
saveFile(filePath: string, data: ArrayBuffer | Blob | string): Promise<number>;
20+
saveFile(filePath: string, data: AttachmentData): Promise<number>;
1821

1922
/**
2023
* Retrieves file data as an ArrayBuffer.

packages/attachments/src/SyncingService.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,22 +107,11 @@ export class SyncingService {
107107
* @returns Updated attachment record with local URI and new state
108108
*/
109109
async downloadAttachment(attachment: AttachmentRecord): Promise<AttachmentRecord> {
110-
console.debug('[SYNC] Downloading:', attachment.id);
111110
try {
112-
const file = await this.remoteStorage.downloadFile(attachment);
113-
console.debug('[SYNC] Downloaded, converting to base64');
114-
const base64Data = await new Promise<string>((resolve, reject) => {
115-
const reader = new FileReader();
116-
reader.onloadend = () => {
117-
// remove the header from the result: 'data:*/*;base64,'
118-
resolve(reader.result?.toString().replace(/^data:.+;base64,/, '') || '');
119-
};
120-
reader.onerror = reject;
121-
reader.readAsDataURL(new File([file], attachment.filename));
122-
});
111+
const fileData = await this.remoteStorage.downloadFile(attachment);
123112

124113
const localUri = this.localStorage.getLocalUri(attachment.filename);
125-
await this.localStorage.saveFile(localUri, base64Data);
114+
await this.localStorage.saveFile(localUri, fileData);
126115

127116
return {
128117
...attachment,

packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js';
1+
import { AttachmentData, EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js';
22

33
/**
44
* IndexDBFileSystemStorageAdapter implements LocalStorageAdapter using IndexedDB.
@@ -41,11 +41,28 @@ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter {
4141
return tx.objectStore('files');
4242
}
4343

44-
async saveFile(filePath: string, data: string): Promise<number> {
44+
async saveFile(filePath: string, data: AttachmentData): Promise<number> {
4545
const store = await this.getStore('readwrite');
46+
47+
let dataToStore: ArrayBuffer;
48+
let size: number;
49+
50+
if (typeof data === 'string') {
51+
const binaryString = atob(data);
52+
const bytes = new Uint8Array(binaryString.length);
53+
for (let i = 0; i < binaryString.length; i++) {
54+
bytes[i] = binaryString.charCodeAt(i);
55+
}
56+
dataToStore = bytes.buffer;
57+
size = bytes.byteLength;
58+
} else {
59+
dataToStore = data;
60+
size = dataToStore.byteLength;
61+
}
62+
4663
return await new Promise<number>((resolve, reject) => {
47-
const req = store.put(data, filePath);
48-
req.onsuccess = () => resolve(data.length);
64+
const req = store.put(dataToStore, filePath);
65+
req.onsuccess = () => resolve(size);
4966
req.onerror = () => reject(req.error);
5067
});
5168
}

packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { promises as fs } from 'fs';
22
import * as path from 'path';
3-
import { EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js';
3+
import { AttachmentData, EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js';
44

55
/**
66
* NodeFileSystemAdapter implements LocalStorageAdapter using Node.js filesystem.
@@ -40,13 +40,17 @@ export class NodeFileSystemAdapter implements LocalStorageAdapter {
4040

4141
async saveFile(
4242
filePath: string,
43-
data: string,
43+
data: AttachmentData,
4444
options?: { encoding?: EncodingType; mediaType?: string }
4545
): Promise<number> {
46-
const buffer = options?.encoding === EncodingType.Base64 ? Buffer.from(data, 'base64') : Buffer.from(data, 'utf8');
47-
await fs.writeFile(filePath, buffer, {
48-
encoding: options?.encoding
49-
});
46+
let buffer: Buffer;
47+
48+
if (typeof data === 'string') {
49+
buffer = Buffer.from(data, options?.encoding ?? EncodingType.Base64);
50+
} else {
51+
buffer = Buffer.from(data);
52+
}
53+
await fs.writeFile(filePath, buffer);
5054
return buffer.length;
5155
}
5256

packages/attachments/tests/attachments.test.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ beforeAll(async () => {
5656
}),
5757
database: {
5858
dbFilename: 'testing.db',
59-
dbLocation: './tests/temp'
6059
}
6160
});
6261

@@ -101,7 +100,6 @@ const watchAttachments = (onUpdate: (attachments: WatchedAttachmentItem[]) => vo
101100

102101
// Helper to watch the attachments table
103102
async function* watchAttachmentsTable(): AsyncGenerator<AttachmentRecord[]> {
104-
console.debug('[TEST] watchAttachmentsTable: watching attachments table');
105103
const watcher = db.watch(
106104
`
107105
SELECT
@@ -113,10 +111,7 @@ async function* watchAttachmentsTable(): AsyncGenerator<AttachmentRecord[]> {
113111
);
114112

115113
for await (const result of watcher) {
116-
console.debug('[TEST] watchAttachmentsTable: result', result);
117114
const attachments = result.rows?._array.map((r: any) => attachmentFromSql(r)) ?? [];
118-
console.debug('[TEST] watchAttachmentsTable: attachments', attachments);
119-
console.debug('[TEST] Mapped attachments:', attachments.map(a => ({ id: a.id?.substring(0,8), state: a.state, hasSynced: a.hasSynced })));
120115
yield attachments;
121116
}
122117
}
@@ -126,7 +121,6 @@ async function waitForMatchCondition(
126121
predicate: (attachments: AttachmentRecord[]) => boolean,
127122
timeoutSeconds: number = 5
128123
): Promise<AttachmentRecord[]> {
129-
console.debug('[TEST] waitForMatchCondition: waiting for condition');
130124
const timeoutMs = timeoutSeconds * 1000;
131125
const abortController = new AbortController();
132126
const startTime = Date.now();
@@ -135,21 +129,16 @@ async function waitForMatchCondition(
135129

136130
try {
137131
for await (const value of generator) {
138-
console.debug('[TEST] waitForMatchCondition: generator value', value);
139132
if (Date.now() - startTime > timeoutMs) {
140-
console.debug('[TEST] waitForMatchCondition: timeout');
141133
throw new Error(`Timeout waiting for condition after ${timeoutSeconds}s`);
142134
}
143135

144136
if (predicate(value)) {
145-
console.debug('[TEST] waitForMatchCondition: match found!');
146137
return value;
147138
}
148139
}
149-
console.debug('[TEST] waitForMatchCondition: for await loop ended without match');
150140
throw new Error('Stream ended without match');
151141
} finally {
152-
console.debug('[TEST] waitForMatchCondition: finally finaling');
153142
await generator.return?.(undefined);
154143
abortController.abort();
155144
}
@@ -188,7 +177,6 @@ describe('attachment queue', () => {
188177
const attachments = await waitForMatchCondition(
189178
() => watchAttachmentsTable(),
190179
(results) => {
191-
console.debug('[TEST] Predicate checking:', results.map(r => ({ id: r.id?.substring(0,8), state: r.state })));
192180
return results.some((r) => r.state === AttachmentState.SYNCED)
193181
},
194182
5
@@ -204,7 +192,7 @@ describe('attachment queue', () => {
204192

205193
// Verify local file exists
206194
const localData = await queue.localStorage.readFile(attachmentRecord.localUri!);
207-
expect(localData).toEqual(MOCK_JPEG_U8A);
195+
expect(Array.from(new Uint8Array(localData))).toEqual(MOCK_JPEG_U8A);
208196

209197
await queue.stopSync();
210198
});

0 commit comments

Comments
 (0)