@pol-studios/powersync 1.0.0 → 1.0.2
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/attachments/index.js +1 -1
- package/dist/{chunk-4FJVBR3X.js → chunk-3AYXHQ4W.js} +24 -13
- package/dist/chunk-3AYXHQ4W.js.map +1 -0
- package/dist/{chunk-BJ36QDFN.js → chunk-7EMDVIZX.js} +1 -1
- package/dist/chunk-7EMDVIZX.js.map +1 -0
- package/dist/chunk-C2RSTGDC.js +726 -0
- package/dist/chunk-C2RSTGDC.js.map +1 -0
- package/dist/{chunk-NPNBGCRC.js → chunk-EJ23MXPQ.js} +1 -1
- package/dist/{chunk-NPNBGCRC.js.map → chunk-EJ23MXPQ.js.map} +1 -1
- package/dist/{chunk-CHRTN5PF.js → chunk-FPTDATY5.js} +1 -1
- package/dist/chunk-FPTDATY5.js.map +1 -0
- package/dist/chunk-GMFDCVMZ.js +1285 -0
- package/dist/chunk-GMFDCVMZ.js.map +1 -0
- package/dist/chunk-OLHGI472.js +1 -0
- package/dist/{chunk-CFCK2LHI.js → chunk-OTJXIRWX.js} +45 -40
- package/dist/chunk-OTJXIRWX.js.map +1 -0
- package/dist/{chunk-GBGATW2S.js → chunk-V6LJ6MR2.js} +86 -95
- package/dist/chunk-V6LJ6MR2.js.map +1 -0
- package/dist/chunk-VJCL2SWD.js +1 -0
- package/dist/chunk-VJCL2SWD.js.map +1 -0
- package/dist/connector/index.d.ts +1 -2
- package/dist/connector/index.js +3 -6
- package/dist/core/index.js +2 -2
- package/dist/{supabase-connector-D14-kl5v.d.ts → index-Cb-NI0Ct.d.ts} +159 -2
- package/dist/index.d.ts +2 -3
- package/dist/index.js +12 -13
- package/dist/index.native.d.ts +1 -2
- package/dist/index.native.js +13 -14
- package/dist/index.web.d.ts +1 -2
- package/dist/index.web.js +13 -14
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/index.native.js +1 -1
- package/dist/platform/index.web.js +1 -1
- package/dist/provider/index.d.ts +6 -1
- package/dist/provider/index.js +6 -6
- package/dist/sync/index.js +3 -3
- package/package.json +35 -10
- package/dist/chunk-4FJVBR3X.js.map +0 -1
- package/dist/chunk-7BPTGEVG.js +0 -1
- package/dist/chunk-BJ36QDFN.js.map +0 -1
- package/dist/chunk-CFCK2LHI.js.map +0 -1
- package/dist/chunk-CHRTN5PF.js.map +0 -1
- package/dist/chunk-FLHDT4TS.js +0 -327
- package/dist/chunk-FLHDT4TS.js.map +0 -1
- package/dist/chunk-GBGATW2S.js.map +0 -1
- package/dist/chunk-Q3LFFMRR.js +0 -925
- package/dist/chunk-Q3LFFMRR.js.map +0 -1
- package/dist/chunk-T225XEML.js +0 -298
- package/dist/chunk-T225XEML.js.map +0 -1
- package/dist/index-nae7nzib.d.ts +0 -147
- /package/dist/{chunk-7BPTGEVG.js.map → chunk-OLHGI472.js.map} +0 -0
package/dist/chunk-FLHDT4TS.js
DELETED
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
classifySupabaseError
|
|
3
|
-
} from "./chunk-CHRTN5PF.js";
|
|
4
|
-
|
|
5
|
-
// src/connector/types.ts
|
|
6
|
-
var defaultSchemaRouter = () => "public";
|
|
7
|
-
|
|
8
|
-
// src/connector/supabase-connector.ts
|
|
9
|
-
var SupabaseConnector = class {
|
|
10
|
-
supabase;
|
|
11
|
-
powerSyncUrl;
|
|
12
|
-
schemaRouter;
|
|
13
|
-
crudHandler;
|
|
14
|
-
logger;
|
|
15
|
-
onTransactionSuccess;
|
|
16
|
-
onTransactionFailure;
|
|
17
|
-
onTransactionComplete;
|
|
18
|
-
shouldUploadFn;
|
|
19
|
-
// Active project IDs for scoped sync (optional feature)
|
|
20
|
-
activeProjectIds = [];
|
|
21
|
-
constructor(options) {
|
|
22
|
-
this.supabase = options.supabaseClient;
|
|
23
|
-
this.powerSyncUrl = options.powerSyncUrl;
|
|
24
|
-
this.schemaRouter = options.schemaRouter ?? defaultSchemaRouter;
|
|
25
|
-
this.crudHandler = options.crudHandler;
|
|
26
|
-
this.logger = options.logger;
|
|
27
|
-
this.onTransactionSuccess = options.onTransactionSuccess;
|
|
28
|
-
this.onTransactionFailure = options.onTransactionFailure;
|
|
29
|
-
this.onTransactionComplete = options.onTransactionComplete;
|
|
30
|
-
this.shouldUploadFn = options.shouldUpload;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Set the active project IDs for scoped sync.
|
|
34
|
-
* Call this when user selects/opens projects.
|
|
35
|
-
*/
|
|
36
|
-
setActiveProjectIds(projectIds) {
|
|
37
|
-
this.activeProjectIds = projectIds;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Get the current active project IDs.
|
|
41
|
-
*/
|
|
42
|
-
getActiveProjectIds() {
|
|
43
|
-
return this.activeProjectIds;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Get credentials for PowerSync connection.
|
|
47
|
-
* Uses Supabase session token.
|
|
48
|
-
*
|
|
49
|
-
* Note: Token refresh is handled by Supabase's startAutoRefresh() which must be
|
|
50
|
-
* called on app initialization. getSession() returns the auto-refreshed token.
|
|
51
|
-
*/
|
|
52
|
-
async fetchCredentials() {
|
|
53
|
-
this.logger?.debug("[Connector] Fetching credentials...");
|
|
54
|
-
const {
|
|
55
|
-
data: { session },
|
|
56
|
-
error
|
|
57
|
-
} = await this.supabase.auth.getSession();
|
|
58
|
-
if (error) {
|
|
59
|
-
this.logger?.error("[Connector] Auth error:", error);
|
|
60
|
-
throw new Error(`Failed to get Supabase session: ${error.message}`);
|
|
61
|
-
}
|
|
62
|
-
if (!session) {
|
|
63
|
-
this.logger?.error("[Connector] No active session");
|
|
64
|
-
throw new Error("No active Supabase session");
|
|
65
|
-
}
|
|
66
|
-
this.logger?.debug(
|
|
67
|
-
"[Connector] Credentials fetched, token expires at:",
|
|
68
|
-
session.expires_at
|
|
69
|
-
);
|
|
70
|
-
return {
|
|
71
|
-
endpoint: this.powerSyncUrl,
|
|
72
|
-
token: session.access_token,
|
|
73
|
-
expiresAt: session.expires_at ? new Date(session.expires_at * 1e3) : void 0
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Upload local changes to Supabase.
|
|
78
|
-
* Called automatically by PowerSync when there are pending uploads.
|
|
79
|
-
*/
|
|
80
|
-
async uploadData(database) {
|
|
81
|
-
if (this.shouldUploadFn && !this.shouldUploadFn()) {
|
|
82
|
-
if (__DEV__) {
|
|
83
|
-
console.log("[Connector] Upload skipped - sync mode does not allow uploads");
|
|
84
|
-
}
|
|
85
|
-
this.logger?.debug("[Connector] Upload skipped - sync mode does not allow uploads");
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (__DEV__) {
|
|
89
|
-
console.log("[Connector] uploadData called, fetching next CRUD transaction...");
|
|
90
|
-
}
|
|
91
|
-
const transaction = await database.getNextCrudTransaction();
|
|
92
|
-
if (!transaction) {
|
|
93
|
-
if (__DEV__) {
|
|
94
|
-
console.log("[Connector] No pending CRUD transaction found");
|
|
95
|
-
}
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (__DEV__) {
|
|
99
|
-
console.log("[Connector] Transaction fetched:", {
|
|
100
|
-
crudCount: transaction.crud.length,
|
|
101
|
-
entries: transaction.crud.map((e) => ({
|
|
102
|
-
table: e.table,
|
|
103
|
-
op: e.op,
|
|
104
|
-
id: e.id,
|
|
105
|
-
opDataKeys: e.opData ? Object.keys(e.opData) : []
|
|
106
|
-
}))
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
for (const entry of transaction.crud) {
|
|
111
|
-
if (__DEV__) {
|
|
112
|
-
console.log("[Connector] Processing CRUD entry:", {
|
|
113
|
-
table: entry.table,
|
|
114
|
-
op: entry.op,
|
|
115
|
-
id: entry.id,
|
|
116
|
-
opData: entry.opData
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
await this.processCrudEntry(entry);
|
|
120
|
-
}
|
|
121
|
-
if (__DEV__) {
|
|
122
|
-
console.log("[Connector] All CRUD entries processed, completing transaction...");
|
|
123
|
-
}
|
|
124
|
-
await transaction.complete();
|
|
125
|
-
if (__DEV__) {
|
|
126
|
-
console.log("[Connector] Transaction completed successfully:", {
|
|
127
|
-
entriesCount: transaction.crud.length
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
this.onTransactionSuccess?.(transaction.crud);
|
|
131
|
-
this.onTransactionComplete?.(transaction.crud);
|
|
132
|
-
} catch (error) {
|
|
133
|
-
const classified = classifySupabaseError(error);
|
|
134
|
-
console.error("[PowerSync Connector] Upload FAILED:", {
|
|
135
|
-
errorMessage: error instanceof Error ? error.message : String(error),
|
|
136
|
-
errorKeys: error && typeof error === "object" ? Object.keys(error) : [],
|
|
137
|
-
errorObject: JSON.stringify(error, null, 2),
|
|
138
|
-
// Full structure
|
|
139
|
-
classified,
|
|
140
|
-
isPermanent: classified.isPermanent,
|
|
141
|
-
// Explicitly log this
|
|
142
|
-
entries: transaction.crud.map((e) => ({
|
|
143
|
-
table: e.table,
|
|
144
|
-
op: e.op,
|
|
145
|
-
id: e.id
|
|
146
|
-
}))
|
|
147
|
-
});
|
|
148
|
-
this.logger?.error("[Connector] Upload error:", {
|
|
149
|
-
error,
|
|
150
|
-
classified,
|
|
151
|
-
entries: transaction.crud.map((e) => ({ table: e.table, op: e.op, id: e.id }))
|
|
152
|
-
});
|
|
153
|
-
this.onTransactionFailure?.(
|
|
154
|
-
transaction.crud,
|
|
155
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
156
|
-
classified
|
|
157
|
-
);
|
|
158
|
-
throw error;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Process a single CRUD operation.
|
|
163
|
-
*
|
|
164
|
-
* UUID-native tables (public schema, post-migration) use `id` as the UUID column.
|
|
165
|
-
* Core schema tables (Profile, Comment, CommentSection) still use a separate `uuid` column.
|
|
166
|
-
*/
|
|
167
|
-
/**
|
|
168
|
-
* Process a single CRUD operation.
|
|
169
|
-
*
|
|
170
|
-
* All synced tables use `id` as their UUID primary key column.
|
|
171
|
-
*/
|
|
172
|
-
async processCrudEntry(entry) {
|
|
173
|
-
const table = entry.table;
|
|
174
|
-
const id = entry.id;
|
|
175
|
-
const schema = this.schemaRouter(table);
|
|
176
|
-
if (this.crudHandler) {
|
|
177
|
-
let handled = false;
|
|
178
|
-
switch (entry.op) {
|
|
179
|
-
case "PUT" /* PUT */:
|
|
180
|
-
handled = await this.crudHandler.handlePut?.(entry, this.supabase, schema) ?? false;
|
|
181
|
-
break;
|
|
182
|
-
case "PATCH" /* PATCH */:
|
|
183
|
-
handled = await this.crudHandler.handlePatch?.(
|
|
184
|
-
entry,
|
|
185
|
-
this.supabase,
|
|
186
|
-
schema
|
|
187
|
-
) ?? false;
|
|
188
|
-
break;
|
|
189
|
-
case "DELETE" /* DELETE */:
|
|
190
|
-
handled = await this.crudHandler.handleDelete?.(
|
|
191
|
-
entry,
|
|
192
|
-
this.supabase,
|
|
193
|
-
schema
|
|
194
|
-
) ?? false;
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
if (handled) {
|
|
198
|
-
this.logger?.debug(
|
|
199
|
-
`[Connector] Custom handler processed ${entry.op} for ${schema}.${table}`
|
|
200
|
-
);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const query = schema === "public" ? this.supabase.from(table) : this.supabase.schema(schema).from(table);
|
|
205
|
-
switch (entry.op) {
|
|
206
|
-
case "PUT" /* PUT */:
|
|
207
|
-
if (__DEV__) {
|
|
208
|
-
console.log("[Connector] Executing PUT/UPSERT:", {
|
|
209
|
-
schema,
|
|
210
|
-
table,
|
|
211
|
-
id,
|
|
212
|
-
data: { id, ...entry.opData }
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
const { data: upsertData, error: upsertError } = await query.upsert(
|
|
216
|
-
{ id, ...entry.opData },
|
|
217
|
-
{ onConflict: "id" }
|
|
218
|
-
).select();
|
|
219
|
-
if (upsertError) {
|
|
220
|
-
if (__DEV__) {
|
|
221
|
-
console.error("[Connector] PUT/UPSERT FAILED:", {
|
|
222
|
-
schema,
|
|
223
|
-
table,
|
|
224
|
-
id,
|
|
225
|
-
error: upsertError,
|
|
226
|
-
errorMessage: upsertError.message,
|
|
227
|
-
errorCode: upsertError.code,
|
|
228
|
-
errorDetails: upsertError.details,
|
|
229
|
-
errorHint: upsertError.hint
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
throw new Error(
|
|
233
|
-
`Upsert failed for ${schema}.${table}: ${upsertError.message}`
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
if (__DEV__) {
|
|
237
|
-
console.log("[Connector] PUT/UPSERT SUCCESS:", {
|
|
238
|
-
schema,
|
|
239
|
-
table,
|
|
240
|
-
id,
|
|
241
|
-
responseData: upsertData
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
break;
|
|
245
|
-
case "PATCH" /* PATCH */:
|
|
246
|
-
if (__DEV__) {
|
|
247
|
-
console.log("[Connector] Executing PATCH/UPDATE:", {
|
|
248
|
-
schema,
|
|
249
|
-
table,
|
|
250
|
-
id,
|
|
251
|
-
opData: entry.opData
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
const { data: updateData, error: updateError } = await query.update(entry.opData).eq("id", id).select();
|
|
255
|
-
if (updateError) {
|
|
256
|
-
if (__DEV__) {
|
|
257
|
-
console.error("[Connector] PATCH/UPDATE FAILED:", {
|
|
258
|
-
schema,
|
|
259
|
-
table,
|
|
260
|
-
id,
|
|
261
|
-
error: updateError,
|
|
262
|
-
errorMessage: updateError.message,
|
|
263
|
-
errorCode: updateError.code,
|
|
264
|
-
errorDetails: updateError.details,
|
|
265
|
-
errorHint: updateError.hint
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
throw new Error(
|
|
269
|
-
`Update failed for ${schema}.${table}: ${updateError.message}`
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
if (__DEV__) {
|
|
273
|
-
console.log("[Connector] PATCH/UPDATE SUCCESS:", {
|
|
274
|
-
schema,
|
|
275
|
-
table,
|
|
276
|
-
id,
|
|
277
|
-
responseData: updateData
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
break;
|
|
281
|
-
case "DELETE" /* DELETE */:
|
|
282
|
-
if (__DEV__) {
|
|
283
|
-
console.log("[Connector] Executing DELETE:", {
|
|
284
|
-
schema,
|
|
285
|
-
table,
|
|
286
|
-
id
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
const { data: deleteData, error: deleteError } = await query.delete().eq("id", id).select();
|
|
290
|
-
if (deleteError) {
|
|
291
|
-
if (__DEV__) {
|
|
292
|
-
console.error("[Connector] DELETE FAILED:", {
|
|
293
|
-
schema,
|
|
294
|
-
table,
|
|
295
|
-
id,
|
|
296
|
-
error: deleteError,
|
|
297
|
-
errorMessage: deleteError.message,
|
|
298
|
-
errorCode: deleteError.code,
|
|
299
|
-
errorDetails: deleteError.details,
|
|
300
|
-
errorHint: deleteError.hint
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
throw new Error(
|
|
304
|
-
`Delete failed for ${schema}.${table}: ${deleteError.message}`
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
if (__DEV__) {
|
|
308
|
-
console.log("[Connector] DELETE SUCCESS:", {
|
|
309
|
-
schema,
|
|
310
|
-
table,
|
|
311
|
-
id,
|
|
312
|
-
responseData: deleteData
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
this.logger?.debug(
|
|
318
|
-
`[Connector] Processed ${entry.op} for ${schema}.${table} (id: ${id})`
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
export {
|
|
324
|
-
defaultSchemaRouter,
|
|
325
|
-
SupabaseConnector
|
|
326
|
-
};
|
|
327
|
-
//# sourceMappingURL=chunk-FLHDT4TS.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/connector/types.ts","../src/connector/supabase-connector.ts"],"sourcesContent":["/**\n * Connector Types for @pol-studios/powersync\n *\n * Defines interfaces for PowerSync backend connectors.\n */\n\nimport type { SupabaseClient } from '@supabase/supabase-js';\nimport type { AbstractPowerSyncDatabase, ClassifiedError, CrudEntry } from '../core/types';\nimport type { LoggerAdapter } from '../platform/types';\n\n// ─── Connector Configuration ─────────────────────────────────────────────────\n\n/**\n * Options for creating a SupabaseConnector\n */\nexport interface SupabaseConnectorOptions {\n /** Supabase client instance */\n supabaseClient: SupabaseClient;\n /** PowerSync service URL */\n powerSyncUrl: string;\n /**\n * Optional: Custom schema routing function.\n * Determines which Supabase schema a table belongs to.\n * @default Returns 'public' for all tables\n */\n schemaRouter?: SchemaRouter;\n /**\n * Optional: Custom CRUD handler for complex mutations.\n * Allows overriding default upsert/update/delete behavior.\n */\n crudHandler?: CrudHandler;\n /** Logger for debugging */\n logger?: LoggerAdapter;\n /** Called when a transaction is successfully uploaded */\n onTransactionSuccess?: (entries: CrudEntry[]) => void;\n /** Called when a transaction fails to upload */\n onTransactionFailure?: (\n entries: CrudEntry[],\n error: Error,\n classified: ClassifiedError\n ) => void;\n /** Called when a transaction is fully completed (after transaction.complete()) */\n onTransactionComplete?: (entries: CrudEntry[]) => void;\n /** Function to check if upload should proceed. Used for sync mode gating. */\n shouldUpload?: () => boolean;\n}\n\n/**\n * Configuration passed to PowerSyncProvider for connector setup\n */\nexport interface ConnectorConfig {\n /**\n * Custom schema routing function.\n * Determines which Supabase schema a table belongs to.\n * @default Returns 'public' for all tables\n *\n * @example\n * ```typescript\n * schemaRouter: (table) => {\n * if (['Profile', 'Comment'].includes(table)) return 'core';\n * return 'public';\n * }\n * ```\n */\n schemaRouter?: SchemaRouter;\n\n /**\n * Custom CRUD handler for complex mutations.\n * @default Uses standard upsert/update/delete operations\n */\n crudHandler?: CrudHandler;\n\n /**\n * Token refresh configuration.\n */\n tokenRefresh?: {\n /** Refresh token when it expires within this many seconds (default: 60) */\n refreshThresholdSeconds?: number;\n };\n}\n\n// ─── Schema Routing ──────────────────────────────────────────────────────────\n\n/**\n * Function that determines which Supabase schema a table belongs to.\n *\n * @param tableName - The name of the table\n * @returns The schema name (e.g., 'public', 'core')\n */\nexport type SchemaRouter = (tableName: string) => string;\n\n/**\n * Default schema router that returns 'public' for all tables\n */\nexport const defaultSchemaRouter: SchemaRouter = () => 'public';\n\n// ─── CRUD Handling ───────────────────────────────────────────────────────────\n\n/**\n * Custom handler for CRUD operations.\n *\n * Return `true` from a handler to indicate the operation was handled.\n * Return `false` to fall back to default behavior.\n */\nexport interface CrudHandler {\n /**\n * Handle a PUT operation (insert or replace).\n * @param entry - The CRUD entry\n * @param supabase - Supabase client\n * @param schema - The resolved schema for this table\n * @returns true if handled, false to use default behavior\n */\n handlePut?(\n entry: CrudEntry,\n supabase: SupabaseClient,\n schema: string\n ): Promise<boolean>;\n\n /**\n * Handle a PATCH operation (update).\n * @param entry - The CRUD entry\n * @param supabase - Supabase client\n * @param schema - The resolved schema for this table\n * @returns true if handled, false to use default behavior\n */\n handlePatch?(\n entry: CrudEntry,\n supabase: SupabaseClient,\n schema: string\n ): Promise<boolean>;\n\n /**\n * Handle a DELETE operation.\n * @param entry - The CRUD entry\n * @param supabase - Supabase client\n * @param schema - The resolved schema for this table\n * @returns true if handled, false to use default behavior\n */\n handleDelete?(\n entry: CrudEntry,\n supabase: SupabaseClient,\n schema: string\n ): Promise<boolean>;\n}\n\n// ─── Credentials ─────────────────────────────────────────────────────────────\n\n/**\n * Credentials returned by fetchCredentials\n */\nexport interface PowerSyncCredentials {\n /** PowerSync service endpoint URL */\n endpoint: string;\n /** JWT token for authentication */\n token: string;\n /** When the token expires */\n expiresAt?: Date;\n}\n\n// ─── Re-export from Core ─────────────────────────────────────────────────────\n\n// Re-export PowerSyncBackendConnector from core/types to maintain API compatibility\nexport type { PowerSyncBackendConnector } from '../core/types';\n","/**\n * Supabase Connector for PowerSync\n *\n * A generic, configurable connector that handles:\n * - Authentication with Supabase JWT tokens\n * - Uploading local changes back to Supabase\n * - Schema routing for multi-schema databases\n * - Custom CRUD handling for complex operations\n */\n\nimport type { SupabaseClient } from '@supabase/supabase-js';\nimport type { AbstractPowerSyncDatabase, ClassifiedError, CrudEntry } from '../core/types';\nimport type { LoggerAdapter } from '../platform/types';\nimport type {\n SupabaseConnectorOptions,\n PowerSyncBackendConnector,\n PowerSyncCredentials,\n SchemaRouter,\n CrudHandler,\n} from './types';\nimport { defaultSchemaRouter } from './types';\nimport { classifySupabaseError } from '../core/errors';\n\n/**\n * Update type enum matching @powersync/common\n */\nenum UpdateType {\n PUT = 'PUT',\n PATCH = 'PATCH',\n DELETE = 'DELETE',\n}\n\n/**\n * Generic Supabase connector for PowerSync.\n *\n * This connector handles authentication and CRUD uploads to Supabase.\n * It supports configurable schema routing and custom CRUD handlers\n * for complex use cases.\n *\n * @example Basic usage\n * ```typescript\n * const connector = new SupabaseConnector({\n * supabaseClient: supabase,\n * powerSyncUrl: 'https://your-powersync-instance.com',\n * });\n * ```\n *\n * @example With schema routing\n * ```typescript\n * const connector = new SupabaseConnector({\n * supabaseClient: supabase,\n * powerSyncUrl: 'https://your-powersync-instance.com',\n * schemaRouter: (table) => {\n * if (['Profile', 'Comment', 'CommentSection'].includes(table)) {\n * return 'core';\n * }\n * return 'public';\n * },\n * });\n * ```\n *\n * @example With custom CRUD handler\n * ```typescript\n * const connector = new SupabaseConnector({\n * supabaseClient: supabase,\n * powerSyncUrl: 'https://your-powersync-instance.com',\n * crudHandler: {\n * async handlePut(entry, supabase, schema) {\n * // Custom handling for specific tables\n * if (entry.table === 'SpecialTable') {\n * await myCustomUpsert(entry);\n * return true; // Handled\n * }\n * return false; // Use default\n * },\n * },\n * });\n * ```\n */\nexport class SupabaseConnector implements PowerSyncBackendConnector {\n private readonly supabase: SupabaseClient;\n private readonly powerSyncUrl: string;\n private readonly schemaRouter: SchemaRouter;\n private readonly crudHandler?: CrudHandler;\n private readonly logger?: LoggerAdapter;\n private readonly onTransactionSuccess?: (entries: CrudEntry[]) => void;\n private readonly onTransactionFailure?: (\n entries: CrudEntry[],\n error: Error,\n classified: ClassifiedError\n ) => void;\n private readonly onTransactionComplete?: (entries: CrudEntry[]) => void;\n private readonly shouldUploadFn?: () => boolean;\n\n // Active project IDs for scoped sync (optional feature)\n private activeProjectIds: string[] = [];\n\n constructor(options: SupabaseConnectorOptions) {\n this.supabase = options.supabaseClient;\n this.powerSyncUrl = options.powerSyncUrl;\n this.schemaRouter = options.schemaRouter ?? defaultSchemaRouter;\n this.crudHandler = options.crudHandler;\n this.logger = options.logger;\n this.onTransactionSuccess = options.onTransactionSuccess;\n this.onTransactionFailure = options.onTransactionFailure;\n this.onTransactionComplete = options.onTransactionComplete;\n this.shouldUploadFn = options.shouldUpload;\n }\n\n /**\n * Set the active project IDs for scoped sync.\n * Call this when user selects/opens projects.\n */\n setActiveProjectIds(projectIds: string[]): void {\n this.activeProjectIds = projectIds;\n }\n\n /**\n * Get the current active project IDs.\n */\n getActiveProjectIds(): string[] {\n return this.activeProjectIds;\n }\n\n /**\n * Get credentials for PowerSync connection.\n * Uses Supabase session token.\n *\n * Note: Token refresh is handled by Supabase's startAutoRefresh() which must be\n * called on app initialization. getSession() returns the auto-refreshed token.\n */\n async fetchCredentials(): Promise<PowerSyncCredentials> {\n this.logger?.debug('[Connector] Fetching credentials...');\n\n const {\n data: { session },\n error,\n } = await this.supabase.auth.getSession();\n\n if (error) {\n this.logger?.error('[Connector] Auth error:', error);\n throw new Error(`Failed to get Supabase session: ${error.message}`);\n }\n\n if (!session) {\n this.logger?.error('[Connector] No active session');\n throw new Error('No active Supabase session');\n }\n\n this.logger?.debug(\n '[Connector] Credentials fetched, token expires at:',\n session.expires_at\n );\n\n return {\n endpoint: this.powerSyncUrl,\n token: session.access_token,\n expiresAt: session.expires_at\n ? new Date(session.expires_at * 1000)\n : undefined,\n };\n }\n\n /**\n * Upload local changes to Supabase.\n * Called automatically by PowerSync when there are pending uploads.\n */\n async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {\n // Check if uploads are allowed based on sync mode\n if (this.shouldUploadFn && !this.shouldUploadFn()) {\n if (__DEV__) {\n console.log('[Connector] Upload skipped - sync mode does not allow uploads');\n }\n this.logger?.debug('[Connector] Upload skipped - sync mode does not allow uploads');\n return;\n }\n\n if (__DEV__) {\n console.log('[Connector] uploadData called, fetching next CRUD transaction...');\n }\n\n const transaction = await database.getNextCrudTransaction();\n\n if (!transaction) {\n if (__DEV__) {\n console.log('[Connector] No pending CRUD transaction found');\n }\n return;\n }\n\n if (__DEV__) {\n console.log('[Connector] Transaction fetched:', {\n crudCount: transaction.crud.length,\n entries: transaction.crud.map(e => ({\n table: e.table,\n op: e.op,\n id: e.id,\n opDataKeys: e.opData ? Object.keys(e.opData) : [],\n })),\n });\n }\n\n try {\n for (const entry of transaction.crud) {\n if (__DEV__) {\n console.log('[Connector] Processing CRUD entry:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n opData: entry.opData,\n });\n }\n await this.processCrudEntry(entry);\n }\n\n if (__DEV__) {\n console.log('[Connector] All CRUD entries processed, completing transaction...');\n }\n\n await transaction.complete();\n\n if (__DEV__) {\n console.log('[Connector] Transaction completed successfully:', {\n entriesCount: transaction.crud.length,\n });\n }\n\n // Notify success\n this.onTransactionSuccess?.(transaction.crud);\n\n // Notify completion (after transaction.complete() succeeds)\n this.onTransactionComplete?.(transaction.crud);\n } catch (error) {\n const classified = classifySupabaseError(error);\n\n // Always log to console for debugging\n console.error('[PowerSync Connector] Upload FAILED:', {\n errorMessage: error instanceof Error ? error.message : String(error),\n errorKeys: error && typeof error === 'object' ? Object.keys(error) : [],\n errorObject: JSON.stringify(error, null, 2), // Full structure\n classified,\n isPermanent: classified.isPermanent, // Explicitly log this\n entries: transaction.crud.map(e => ({\n table: e.table,\n op: e.op,\n id: e.id,\n })),\n });\n\n this.logger?.error('[Connector] Upload error:', {\n error,\n classified,\n entries: transaction.crud.map(e => ({ table: e.table, op: e.op, id: e.id })),\n });\n\n // Notify failure with classification\n this.onTransactionFailure?.(\n transaction.crud,\n error instanceof Error ? error : new Error(String(error)),\n classified\n );\n\n // Re-throw for PowerSync's native retry mechanism\n throw error;\n }\n }\n\n /**\n * Process a single CRUD operation.\n *\n * UUID-native tables (public schema, post-migration) use `id` as the UUID column.\n * Core schema tables (Profile, Comment, CommentSection) still use a separate `uuid` column.\n */\n /**\n * Process a single CRUD operation.\n *\n * All synced tables use `id` as their UUID primary key column.\n */\n private async processCrudEntry(entry: CrudEntry): Promise<void> {\n const table = entry.table;\n const id = entry.id; // PowerSync sends UUID as the entry.id\n const schema = this.schemaRouter(table);\n\n // Try custom handler first\n if (this.crudHandler) {\n let handled = false;\n\n switch (entry.op) {\n case UpdateType.PUT:\n handled =\n (await this.crudHandler.handlePut?.(entry, this.supabase, schema)) ??\n false;\n break;\n case UpdateType.PATCH:\n handled =\n (await this.crudHandler.handlePatch?.(\n entry,\n this.supabase,\n schema\n )) ?? false;\n break;\n case UpdateType.DELETE:\n handled =\n (await this.crudHandler.handleDelete?.(\n entry,\n this.supabase,\n schema\n )) ?? false;\n break;\n }\n\n if (handled) {\n this.logger?.debug(\n `[Connector] Custom handler processed ${entry.op} for ${schema}.${table}`\n );\n return;\n }\n }\n\n // Default behavior\n // Get the correct Supabase query builder for this table's schema\n const query =\n schema === 'public'\n ? this.supabase.from(table)\n : (this.supabase.schema(schema) as unknown as ReturnType<typeof this.supabase.schema>).from(table);\n\n switch (entry.op) {\n case UpdateType.PUT:\n if (__DEV__) {\n console.log('[Connector] Executing PUT/UPSERT:', {\n schema,\n table,\n id,\n data: { id, ...entry.opData },\n });\n }\n // Insert/upsert using id column\n const { data: upsertData, error: upsertError } = await query.upsert(\n { id, ...entry.opData },\n { onConflict: 'id' }\n ).select();\n\n if (upsertError) {\n if (__DEV__) {\n console.error('[Connector] PUT/UPSERT FAILED:', {\n schema,\n table,\n id,\n error: upsertError,\n errorMessage: upsertError.message,\n errorCode: upsertError.code,\n errorDetails: upsertError.details,\n errorHint: upsertError.hint,\n });\n }\n throw new Error(\n `Upsert failed for ${schema}.${table}: ${upsertError.message}`\n );\n }\n if (__DEV__) {\n console.log('[Connector] PUT/UPSERT SUCCESS:', {\n schema,\n table,\n id,\n responseData: upsertData,\n });\n }\n break;\n\n case UpdateType.PATCH:\n if (__DEV__) {\n console.log('[Connector] Executing PATCH/UPDATE:', {\n schema,\n table,\n id,\n opData: entry.opData,\n });\n }\n // Update by id column\n const { data: updateData, error: updateError } = await query\n .update(entry.opData)\n .eq('id', id)\n .select();\n\n if (updateError) {\n if (__DEV__) {\n console.error('[Connector] PATCH/UPDATE FAILED:', {\n schema,\n table,\n id,\n error: updateError,\n errorMessage: updateError.message,\n errorCode: updateError.code,\n errorDetails: updateError.details,\n errorHint: updateError.hint,\n });\n }\n throw new Error(\n `Update failed for ${schema}.${table}: ${updateError.message}`\n );\n }\n if (__DEV__) {\n console.log('[Connector] PATCH/UPDATE SUCCESS:', {\n schema,\n table,\n id,\n responseData: updateData,\n });\n }\n break;\n\n case UpdateType.DELETE:\n if (__DEV__) {\n console.log('[Connector] Executing DELETE:', {\n schema,\n table,\n id,\n });\n }\n // Delete by id column\n const { data: deleteData, error: deleteError } = await query.delete().eq('id', id).select();\n\n if (deleteError) {\n if (__DEV__) {\n console.error('[Connector] DELETE FAILED:', {\n schema,\n table,\n id,\n error: deleteError,\n errorMessage: deleteError.message,\n errorCode: deleteError.code,\n errorDetails: deleteError.details,\n errorHint: deleteError.hint,\n });\n }\n throw new Error(\n `Delete failed for ${schema}.${table}: ${deleteError.message}`\n );\n }\n if (__DEV__) {\n console.log('[Connector] DELETE SUCCESS:', {\n schema,\n table,\n id,\n responseData: deleteData,\n });\n }\n break;\n }\n\n this.logger?.debug(\n `[Connector] Processed ${entry.op} for ${schema}.${table} (id: ${id})`\n );\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/attachments/types.ts","../src/attachments/attachment-queue.ts"],"sourcesContent":["/**\n * Attachment Queue Types for @pol-studios/powersync\n *\n * Defines interfaces for the attachment queue system that handles\n * offline file caching with download, compression, and eviction.\n */\n\n// ─── Attachment States ───────────────────────────────────────────────────────\n\n/**\n * State of an attachment in the queue.\n * NOTE: These do NOT match @powersync/attachments enum ordering (values 0-2 differ).\n */\nexport enum AttachmentState {\n /** Waiting to be downloaded */\n QUEUED_DOWNLOAD = 0,\n /** Waiting for initial sync */\n QUEUED_SYNC = 1,\n /** Waiting to be uploaded */\n QUEUED_UPLOAD = 2,\n /** Fully synced (downloaded or uploaded) */\n SYNCED = 3,\n /** Archived (removed from sync but record kept) */\n ARCHIVED = 4,\n}\n\n// ─── Attachment Record ───────────────────────────────────────────────────────\n\n/**\n * Record representing an attachment in the queue database.\n */\nexport interface AttachmentRecord {\n /** Unique identifier (typically storage path) */\n id: string;\n /** Filename for display and type inference */\n filename: string;\n /** MIME type of the file */\n media_type: string;\n /** Current state in the queue */\n state: AttachmentState;\n /** Local file URI (set after download) */\n local_uri?: string | null;\n /** File size in bytes */\n size?: number;\n /** Timestamp when the attachment was created */\n timestamp?: number;\n}\n\n// ─── Source Table Configuration ──────────────────────────────────────────────\n\n/**\n * Configuration for the source table that contains attachment references.\n * This makes the attachment queue reusable across different tables/projects.\n */\nexport interface AttachmentSourceConfig {\n /**\n * Table name containing attachment references.\n * @example \"EquipmentUnitMediaContent\"\n */\n table: string;\n\n /**\n * Column containing the storage path / attachment ID.\n * @example \"storagePath\"\n */\n idColumn: string;\n\n /**\n * Column to order by for \"newest first\" downloads.\n * Set to null to skip ordering.\n * @example \"takenOn\"\n */\n orderByColumn: string | null;\n\n /**\n * Optional filter config to exclude attachments from archived/unsynced projects.\n * When set, the attachment query JOINs through intermediary tables to ensure\n * only attachments belonging to synced projects are downloaded.\n */\n projectFilter?: {\n /** Foreign key column in the source table (e.g., \"equipmentUnitId\") */\n foreignKey: string;\n /** Intermediary table to JOIN through (e.g., \"EquipmentFixtureUnit\") */\n intermediaryTable: string;\n /** Column in intermediary table that links to ProjectDatabase (e.g., \"projectDatabaseId\") */\n projectForeignKey: string;\n };\n}\n\n// ─── Storage Adapter ─────────────────────────────────────────────────────────\n\n/**\n * Interface for attachment storage operations (e.g., Supabase Storage).\n */\nexport interface AttachmentStorageAdapter {\n /**\n * Download a file from remote storage.\n * @param filePath - The storage path of the file\n * @returns The file data as a Blob or base64 string\n */\n downloadFile(filePath: string): Promise<Blob | string>;\n\n /**\n * Upload a file to remote storage (optional - not all queues need upload).\n * @param filePath - The storage path to upload to\n * @param data - The file data to upload\n */\n uploadFile?(filePath: string, data: Blob | string): Promise<void>;\n\n /**\n * Delete a file from remote storage (optional).\n * @param filePath - The storage path of the file\n */\n deleteFile?(filePath: string): Promise<void>;\n\n /**\n * Resolve the storage bucket for a file path.\n * Allows routing different files to different buckets.\n * @param filePath - The file path to resolve\n * @returns The bucket name\n */\n resolveBucket?(filePath: string): string;\n}\n\n// ─── Compression Configuration ───────────────────────────────────────────────\n\n/**\n * Configuration for image compression.\n */\nexport interface CompressionConfig {\n /** Enable compression (default: true) */\n enabled: boolean;\n /** Compression quality 0.0-1.0 (default: 0.7) */\n quality: number;\n /** Max width before resizing (default: 2048) */\n maxWidth: number;\n /** Skip files under this size in bytes (default: 100KB) */\n skipSizeBytes: number;\n /** Skip if already under this size in bytes (default: 300KB) */\n targetSizeBytes: number;\n}\n\n/**\n * Default compression configuration.\n */\nexport const DEFAULT_COMPRESSION_CONFIG: CompressionConfig = {\n enabled: true,\n quality: 0.7,\n maxWidth: 2048,\n skipSizeBytes: 100_000,\n targetSizeBytes: 300_000,\n};\n\n// ─── Download Configuration ──────────────────────────────────────────────────\n\n/**\n * Configuration for the download engine.\n */\nexport interface DownloadConfig {\n /** Maximum concurrent downloads (default: 50) */\n concurrency: number;\n /** Download timeout per file in ms (default: 60000) */\n timeoutMs: number;\n /** Retry delay between batches in ms (default: 5000) */\n retryDelayMs: number;\n}\n\n/**\n * Default download configuration.\n */\nexport const DEFAULT_DOWNLOAD_CONFIG: DownloadConfig = {\n concurrency: 50,\n timeoutMs: 60_000,\n retryDelayMs: 5_000,\n};\n\n// ─── Cache Configuration ─────────────────────────────────────────────────────\n\n/**\n * Configuration for cache management.\n */\nexport interface CacheConfig {\n /** Maximum cache size in bytes (default: 5GB) */\n maxSize: number;\n /** Stop downloads at this percentage of max (default: 0.95 = 95%) */\n downloadStopThreshold: number;\n /** Trigger eviction at this percentage (default: 1.0 = 100%) */\n evictionTriggerThreshold: number;\n}\n\n/**\n * Default cache configuration.\n */\nexport const DEFAULT_CACHE_CONFIG: CacheConfig = {\n maxSize: 5 * 1024 * 1024 * 1024, // 5 GB\n downloadStopThreshold: 0.95,\n evictionTriggerThreshold: 1.0,\n};\n\n// ─── Attachment Queue Configuration ──────────────────────────────────────────\n\n/**\n * Full configuration for the attachment queue.\n */\nexport interface AttachmentQueueConfig {\n /** Source table configuration */\n source: AttachmentSourceConfig;\n\n /** Storage adapter for downloading files */\n storage: AttachmentStorageAdapter;\n\n /** Table name for storing attachment records (default: \"photo_attachments\") */\n attachmentTableName?: string;\n\n /** Perform initial sync on initialization (default: true) */\n performInitialSync?: boolean;\n\n /** Download configuration */\n download?: Partial<DownloadConfig>;\n\n /** Cache configuration */\n cache?: Partial<CacheConfig>;\n\n /** Compression configuration */\n compression?: Partial<CompressionConfig>;\n\n /**\n * Called when a download fails.\n * Return { retry: true } to retry, { retry: false } to skip.\n */\n onDownloadError?: (\n attachment: AttachmentRecord,\n error: Error\n ) => Promise<{ retry: boolean }>;\n\n /**\n * Called when sync progress changes.\n */\n onProgress?: (stats: AttachmentSyncStats) => void;\n}\n\n// ─── Download Status Types ───────────────────────────────────────────────────\n\n/**\n * Current phase of a download operation.\n */\nexport type DownloadPhase = 'downloading' | 'compressing' | 'complete' | 'error';\n\n/**\n * Status of an individual download.\n */\nexport interface DownloadStatus {\n /** Attachment ID */\n id: string;\n /** Filename being downloaded */\n filename: string;\n /** Current phase */\n phase: DownloadPhase;\n}\n\n// ─── Sync Status Types ───────────────────────────────────────────────────────\n\n/**\n * Why downloads are stopped (if not actively syncing).\n */\nexport type AttachmentSyncStatus =\n | 'syncing' // Actively downloading\n | 'paused' // User manually paused\n | 'cache_full' // Stopped because cache hit capacity\n | 'complete'; // All attachments downloaded\n\n/**\n * Statistics about the attachment sync progress.\n */\nexport interface AttachmentSyncStats {\n /** Number of attachments that have been downloaded */\n syncedCount: number;\n /** Total size of synced attachments in bytes */\n syncedSize: number;\n /** Number of attachments waiting to be downloaded */\n pendingCount: number;\n /** Total expected attachments (synced + pending) */\n totalExpected: number;\n /** Maximum cache size in bytes */\n maxCacheSize: number;\n /** Current compression quality (0.1 to 1.0) */\n compressionQuality: number;\n /** Current sync status */\n status: AttachmentSyncStatus;\n /** Whether downloads are paused */\n isPaused: boolean;\n /** Whether currently processing downloads */\n isProcessing: boolean;\n /** Currently active downloads */\n activeDownloads: DownloadStatus[];\n}\n\n// ─── SQL Row Types (for internal use) ────────────────────────────────────────\n\n/** Row from stats query */\nexport interface AttachmentStatsRow {\n state: number;\n cnt: number;\n sz: number;\n}\n\n/** Row for cache file operations */\nexport interface CacheFileRow {\n id: string;\n local_uri: string;\n}\n\n/** Row for eviction operations */\nexport interface EvictRow {\n id: string;\n local_uri: string;\n size: number;\n}\n\n/** Row for cached size queries */\nexport interface CachedSizeRow {\n total: number;\n}\n\n/** Row for ID queries */\nexport interface IdRow {\n id: string;\n}\n","/**\n * Attachment Queue for @pol-studios/powersync\n *\n * Platform-agnostic attachment queue that handles:\n * - Parallel downloads with configurable concurrency\n * - Cache management with eviction\n * - Image compression\n * - Pause/resume functionality\n * - Progress tracking\n */\n\nimport type { AbstractPowerSyncDatabase } from '../core/types';\nimport type { PlatformAdapter, LoggerAdapter } from '../platform/types';\nimport {\n AttachmentState,\n type AttachmentRecord,\n type AttachmentQueueConfig,\n type AttachmentSyncStats,\n type AttachmentSyncStatus,\n type DownloadStatus,\n type DownloadPhase,\n type AttachmentStatsRow,\n type CacheFileRow,\n type EvictRow,\n type CachedSizeRow,\n type IdRow,\n DEFAULT_COMPRESSION_CONFIG,\n DEFAULT_DOWNLOAD_CONFIG,\n DEFAULT_CACHE_CONFIG,\n} from './types';\n\n// ─── Internal Types ──────────────────────────────────────────────────────────\n\ninterface DownloadResult {\n id: string;\n size: number;\n}\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\nconst NOTIFY_THROTTLE_MS = 500;\nconst STATS_CACHE_TTL_MS = 500;\nconst CACHE_SIZE_TTL_MS = 5000;\n\n/**\n * Platform-agnostic attachment queue for offline file caching.\n *\n * @example\n * ```typescript\n * const queue = new AttachmentQueue({\n * powersync: db,\n * platform: platformAdapter,\n * config: {\n * source: {\n * table: 'EquipmentUnitMediaContent',\n * idColumn: 'storagePath',\n * orderByColumn: 'takenOn',\n * },\n * storage: storageAdapter,\n * },\n * });\n *\n * await queue.init();\n * ```\n */\nexport class AttachmentQueue {\n // ─── Dependencies ──────────────────────────────────────────────────────────\n\n private readonly powersync: AbstractPowerSyncDatabase;\n private readonly platform: PlatformAdapter;\n private readonly config: AttachmentQueueConfig;\n private readonly logger: LoggerAdapter;\n private readonly tableName: string;\n\n // ─── Configuration ─────────────────────────────────────────────────────────\n\n private readonly concurrency: number;\n private readonly downloadTimeoutMs: number;\n private readonly retryDelayMs: number;\n private readonly downloadStopThreshold: number;\n private readonly evictionTriggerThreshold: number;\n\n // ─── State ─────────────────────────────────────────────────────────────────\n\n private _initialized = false;\n private _paused = false;\n private _processing = false;\n private _cacheFull = false;\n private _resumeRequested = false;\n private _maxCacheSize: number;\n private _compressionQuality: number;\n private _abort = new AbortController();\n private _downloads = new Map<string, DownloadStatus>();\n\n // ─── Caching ───────────────────────────────────────────────────────────────\n\n private _cachedSize = 0;\n private _cachedSizeTimestamp = 0;\n private _cachedStats: AttachmentSyncStats | null = null;\n private _cachedStatsTimestamp = 0;\n\n // ─── Notification ──────────────────────────────────────────────────────────\n\n private _lastNotifyTime = 0;\n private _notifyTimer: ReturnType<typeof setTimeout> | null = null;\n private _progressCallbacks = new Set<(stats: AttachmentSyncStats) => void>();\n private _watcherInterval: ReturnType<typeof setInterval> | null = null;\n\n // ─── Constructor ───────────────────────────────────────────────────────────\n\n constructor(options: {\n powersync: AbstractPowerSyncDatabase;\n platform: PlatformAdapter;\n config: AttachmentQueueConfig;\n }) {\n this.powersync = options.powersync;\n this.platform = options.platform;\n this.config = options.config;\n this.logger = options.platform.logger;\n this.tableName = options.config.attachmentTableName ?? 'photo_attachments';\n\n // Merge with defaults\n const downloadConfig = { ...DEFAULT_DOWNLOAD_CONFIG, ...options.config.download };\n const cacheConfig = { ...DEFAULT_CACHE_CONFIG, ...options.config.cache };\n const compressionConfig = { ...DEFAULT_COMPRESSION_CONFIG, ...options.config.compression };\n\n this.concurrency = downloadConfig.concurrency;\n this.downloadTimeoutMs = downloadConfig.timeoutMs;\n this.retryDelayMs = downloadConfig.retryDelayMs;\n this.downloadStopThreshold = cacheConfig.downloadStopThreshold;\n this.evictionTriggerThreshold = cacheConfig.evictionTriggerThreshold;\n this._maxCacheSize = cacheConfig.maxSize;\n this._compressionQuality = compressionConfig.quality;\n }\n\n // ─── Initialization ────────────────────────────────────────────────────────\n\n /**\n * Initialize the attachment queue.\n * Creates the attachment table if needed and starts watching for downloads.\n */\n async init(): Promise<void> {\n if (this._initialized) return;\n\n this.logger.info('[AttachmentQueue] Initializing...');\n\n // Create attachment table\n await this.powersync.execute(`\n CREATE TABLE IF NOT EXISTS ${this.tableName} (\n id TEXT PRIMARY KEY,\n filename TEXT NOT NULL,\n media_type TEXT,\n state INTEGER NOT NULL DEFAULT ${AttachmentState.QUEUED_DOWNLOAD},\n local_uri TEXT,\n size INTEGER DEFAULT 0,\n timestamp INTEGER\n )\n `);\n\n this._initialized = true;\n\n // Start watching for downloads if configured\n if (this.config.performInitialSync !== false) {\n this._startDownloadWatcher();\n }\n\n this.logger.info('[AttachmentQueue] Initialized');\n }\n\n /**\n * Dispose the attachment queue.\n */\n dispose(): void {\n this._abort.abort();\n if (this._watcherInterval) {\n clearInterval(this._watcherInterval);\n this._watcherInterval = null;\n }\n if (this._notifyTimer) {\n clearTimeout(this._notifyTimer);\n }\n this._progressCallbacks.clear();\n this._initialized = false;\n }\n\n // ─── Public API ────────────────────────────────────────────────────────────\n\n /**\n * Subscribe to real-time progress updates.\n * Returns an unsubscribe function.\n */\n onProgress(callback: (stats: AttachmentSyncStats) => void): () => void {\n this._progressCallbacks.add(callback);\n // Immediately notify with current stats\n this._notify(true);\n return () => {\n this._progressCallbacks.delete(callback);\n };\n }\n\n /** Whether downloads are paused */\n get paused(): boolean {\n return this._paused;\n }\n\n /** Whether currently processing downloads */\n get processing(): boolean {\n return this._processing;\n }\n\n /**\n * Set the maximum cache size.\n */\n async setMaxCacheSize(bytes: number): Promise<void> {\n const oldLimit = this._maxCacheSize;\n this._maxCacheSize = bytes;\n this.logger.info(\n `[AttachmentQueue] Cache limit changed: ${Math.round(oldLimit / 1024 / 1024)}MB → ${Math.round(bytes / 1024 / 1024)}MB`\n );\n\n if (bytes < oldLimit) {\n const currentSize = await this._getCachedSizeWithCache();\n const downloadStopLimit = bytes * this.downloadStopThreshold;\n\n if (currentSize >= downloadStopLimit) {\n this._cacheFull = true;\n this._abort.abort();\n this._abort = new AbortController();\n this._notify(true);\n await this._evictIfNeeded();\n } else {\n this._notify(true);\n }\n } else if (bytes > oldLimit && this._cacheFull) {\n this._cacheFull = false;\n this._notify(true);\n if (!this._paused && !this._processing) {\n this._startDownloads();\n }\n } else {\n this._notify(true);\n }\n }\n\n /**\n * Set the compression quality (0.1 to 1.0).\n */\n setCompressionQuality(quality: number): void {\n this._compressionQuality = Math.max(0.1, Math.min(1.0, quality));\n this._notify(true);\n }\n\n /** Get the current compression quality */\n getCompressionQuality(): number {\n return this._compressionQuality;\n }\n\n /**\n * Pause downloads.\n */\n pause(): void {\n this.logger.info('[AttachmentQueue] Pausing downloads');\n this._paused = true;\n this._abort.abort();\n this._abort = new AbortController();\n this._notify(true);\n }\n\n /**\n * Resume downloads.\n */\n resume(): void {\n this.logger.info('[AttachmentQueue] Resuming downloads');\n this._paused = false;\n this._abort = new AbortController();\n this._notify(true);\n\n if (this._processing) {\n this._resumeRequested = true;\n } else {\n this._startDownloads();\n }\n }\n\n /**\n * Get current sync statistics.\n */\n async getStats(): Promise<AttachmentSyncStats> {\n const now = Date.now();\n\n if (\n this._cachedStats &&\n now - this._cachedStatsTimestamp < STATS_CACHE_TTL_MS\n ) {\n return {\n ...this._cachedStats,\n compressionQuality: this._compressionQuality,\n status: this._getStatus(this._cachedStats.pendingCount),\n isPaused: this._paused,\n isProcessing: this._processing,\n activeDownloads: [...this._downloads.values()],\n };\n }\n\n const rows = await this.powersync.getAll<AttachmentStatsRow>(\n `SELECT state, COUNT(*) as cnt, COALESCE(SUM(size), 0) as sz\n FROM ${this.tableName} GROUP BY state`\n );\n\n let synced = 0;\n let syncedSize = 0;\n let pending = 0;\n\n for (const r of rows) {\n if (r.state === AttachmentState.SYNCED) {\n synced = r.cnt;\n syncedSize = r.sz;\n }\n if (\n r.state === AttachmentState.QUEUED_DOWNLOAD ||\n r.state === AttachmentState.QUEUED_SYNC\n ) {\n pending += r.cnt;\n }\n }\n\n this._cachedSize = syncedSize;\n this._cachedSizeTimestamp = now;\n\n const stats: AttachmentSyncStats = {\n syncedCount: synced,\n syncedSize,\n pendingCount: pending,\n totalExpected: synced + pending,\n maxCacheSize: this._maxCacheSize,\n compressionQuality: this._compressionQuality,\n status: this._getStatus(pending),\n isPaused: this._paused,\n isProcessing: this._processing,\n activeDownloads: [...this._downloads.values()],\n };\n\n this._cachedStats = stats;\n this._cachedStatsTimestamp = now;\n\n return stats;\n }\n\n /**\n * Clear all cached files and re-queue for download.\n */\n async clearCache(): Promise<void> {\n const wasPaused = this._paused;\n if (!wasPaused) this.pause();\n\n try {\n // Wait for processing to stop\n let attempts = 0;\n while (this._processing && attempts < 50) {\n await this._sleep(100, this._abort.signal);\n attempts++;\n }\n\n // Delete all cached files\n const withFiles = await this.powersync.getAll<CacheFileRow>(\n `SELECT id, local_uri FROM ${this.tableName} WHERE local_uri IS NOT NULL`\n );\n\n for (const row of withFiles) {\n try {\n await this.platform.fileSystem.deleteFile(row.local_uri);\n } catch {\n // File may already be gone\n }\n }\n\n // Re-queue all records\n await this.powersync.execute(\n `UPDATE ${this.tableName} SET state = ?, local_uri = NULL, size = 0`,\n [AttachmentState.QUEUED_DOWNLOAD]\n );\n } finally {\n this._cachedSize = 0;\n this._cachedSizeTimestamp = 0;\n this._cachedStats = null;\n this._cachedStatsTimestamp = 0;\n this._cacheFull = false;\n\n if (!wasPaused) {\n this.resume();\n } else {\n this._notify(true);\n }\n }\n }\n\n /**\n * Get an attachment record by ID.\n */\n async getRecord(id: string): Promise<AttachmentRecord | null> {\n return this.powersync.get<AttachmentRecord>(\n `SELECT * FROM ${this.tableName} WHERE id = ?`,\n [id]\n );\n }\n\n /**\n * Get the local file URI for an attachment.\n */\n getLocalUri(localPath: string): string {\n const cacheDir = this.platform.fileSystem.getCacheDirectory();\n return `${cacheDir}attachments/${localPath}`;\n }\n\n /**\n * Cache a local file (e.g. one just uploaded) into the attachment cache.\n * This avoids a redundant download by copying the source file directly\n * into the cache directory and marking it as SYNCED.\n */\n async cacheLocalFile(storagePath: string, sourceUri: string): Promise<void> {\n if (!this._initialized) {\n this.logger.warn('[AttachmentQueue] cacheLocalFile called before init');\n return;\n }\n\n try {\n // Verify source file exists (temp files can be cleaned by OS)\n const sourceInfo = await this.platform.fileSystem.getFileInfo(sourceUri);\n if (!sourceInfo || !sourceInfo.exists) {\n this.logger.warn(`[AttachmentQueue] Source file does not exist: ${sourceUri}`);\n return;\n }\n\n // Same path sanitization as _downloadOne\n const localPath = storagePath.replace(/[^a-zA-Z0-9]/g, '_');\n const localUri = this.getLocalUri(localPath);\n\n // Ensure cache directory exists\n const dir = localUri.substring(0, localUri.lastIndexOf('/'));\n await this.platform.fileSystem.makeDirectory(dir, { intermediates: true });\n\n // Copy uploaded file into cache\n await this.platform.fileSystem.copyFile(sourceUri, localUri);\n\n // Get file size for cache tracking\n const info = await this.platform.fileSystem.getFileInfo(localUri);\n const size = (info && info.exists) ? info.size : 0;\n\n // Infer media type from extension\n const ext = storagePath.split('.').pop()?.toLowerCase() ?? '';\n const mediaTypeMap: Record<string, string> = {\n jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',\n webp: 'image/webp', heic: 'image/heic', heif: 'image/heif',\n gif: 'image/gif', mp4: 'video/mp4', mov: 'video/quicktime',\n };\n const mediaType = mediaTypeMap[ext] ?? 'application/octet-stream';\n\n // INSERT OR REPLACE as SYNCED\n await this.powersync.execute(\n `INSERT OR REPLACE INTO ${this.tableName} (id, filename, media_type, state, local_uri, size, timestamp)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n [storagePath, storagePath, mediaType, AttachmentState.SYNCED, localPath, size, Date.now()]\n );\n\n // Invalidate stats caches\n this._cachedStats = null;\n this._cachedStatsTimestamp = 0;\n this._cachedSizeTimestamp = 0;\n\n // Notify subscribers\n this._notify(true);\n\n this.logger.info(`[AttachmentQueue] Cached local file: ${storagePath} (${Math.round(size / 1024)}KB)`);\n } catch (err) {\n this.logger.warn(`[AttachmentQueue] Failed to cache local file: ${storagePath}`, err);\n }\n }\n\n // ─── Download Watcher ──────────────────────────────────────────────────────\n\n private _startDownloadWatcher(): void {\n const { table, idColumn, projectFilter } = this.config.source;\n\n let query: string;\n if (projectFilter) {\n const { foreignKey, intermediaryTable, projectForeignKey } = projectFilter;\n query = `\n SELECT m.${idColumn} as id\n FROM ${table} m\n JOIN ${intermediaryTable} u ON m.${foreignKey} = u.id\n JOIN ProjectDatabase p ON u.${projectForeignKey} = p.id\n WHERE m.${idColumn} IS NOT NULL AND m.${idColumn} != ''\n `;\n } else {\n query = `SELECT ${idColumn} as id FROM ${table}\n WHERE ${idColumn} IS NOT NULL AND ${idColumn} != ''`;\n }\n\n // Use a simple interval-based approach since we can't rely on powersync.watch\n const checkForNewAttachments = async () => {\n // Check if disposed or aborted before executing\n if (this._abort.signal.aborted || !this._initialized) {\n return;\n }\n\n try {\n const result = await this.powersync.getAll<IdRow>(query);\n // Handle potential null/undefined result (defensive)\n const ids = (result ?? []).map((r) => r.id);\n\n // No attachments found - this is normal for new projects\n if (ids.length === 0) {\n return;\n }\n\n // Sync new attachments to queue\n await this._syncAttachmentIds(ids);\n\n // Start downloads if not paused\n if (!this._paused && !this._processing) {\n this._startDownloads();\n }\n } catch (err) {\n // Handle expected conditions gracefully (new projects, initial sync)\n const errorMessage = String(err);\n if (\n errorMessage.includes('Result set is empty') ||\n errorMessage.includes('no such table') ||\n errorMessage.includes('SQLITE_EMPTY')\n ) {\n // This is expected during initial sync or for new projects - log at debug level\n this.logger.debug('[AttachmentQueue] No attachments found in source table (expected for new projects)');\n return;\n }\n this.logger.warn('[AttachmentQueue] Watch error:', err);\n }\n };\n\n // Initial check\n checkForNewAttachments();\n\n // Periodic check every 30 seconds - store reference for cleanup\n this._watcherInterval = setInterval(checkForNewAttachments, 30000);\n }\n\n private async _syncAttachmentIds(ids: string[]): Promise<void> {\n for (const id of ids) {\n const existing = await this.getRecord(id);\n if (!existing) {\n await this.powersync.execute(\n `INSERT OR IGNORE INTO ${this.tableName} (id, filename, state, timestamp)\n VALUES (?, ?, ?, ?)`,\n [id, id, AttachmentState.QUEUED_DOWNLOAD, Date.now()]\n );\n }\n }\n }\n\n // ─── Download Engine ───────────────────────────────────────────────────────\n\n private async _startDownloads(): Promise<void> {\n if (this._paused || this._processing) {\n if (this._processing) {\n this._resumeRequested = true;\n }\n return;\n }\n\n this.logger.info('[AttachmentQueue] Starting downloads');\n this._processing = true;\n this._resumeRequested = false;\n const signal = this._abort.signal;\n\n try {\n let toProcess = await this._getIdsToDownload();\n let prevQueueSize = 0;\n\n while (toProcess.length > 0 && !signal.aborted) {\n const ordered = await this._orderByNewest(toProcess);\n let currentCachedSize = await this._getCachedSizeWithCache();\n const downloadStopLimit = this._maxCacheSize * this.downloadStopThreshold;\n\n if (currentCachedSize >= downloadStopLimit) {\n this._cacheFull = true;\n this.logger.info('[AttachmentQueue] Cache at capacity, stopping downloads');\n break;\n }\n\n this._cacheFull = false;\n\n for (let i = 0; i < ordered.length; i += this.concurrency) {\n if (signal.aborted) break;\n\n if (currentCachedSize >= downloadStopLimit) {\n this._cacheFull = true;\n break;\n }\n\n const chunk = ordered.slice(i, i + this.concurrency);\n const results = await Promise.allSettled(\n chunk.map((id) => this._downloadOne(id, signal))\n );\n\n const successful: DownloadResult[] = [];\n for (const result of results) {\n if (result.status === 'fulfilled' && result.value) {\n successful.push(result.value);\n }\n }\n\n if (successful.length > 0) {\n await this._batchUpdateSizes(successful);\n currentCachedSize = await this._getCachedSizeWithCache(true);\n }\n }\n\n if (signal.aborted) break;\n\n toProcess = await this._getIdsToDownload();\n\n if (toProcess.length > 0 && toProcess.length >= prevQueueSize) {\n await this._sleep(this.retryDelayMs, signal);\n }\n prevQueueSize = toProcess.length;\n }\n } catch (err) {\n this.logger.error('[AttachmentQueue] Download loop error:', err);\n } finally {\n this._processing = false;\n this._notify(true);\n\n if (this._resumeRequested && !this._paused) {\n this._resumeRequested = false;\n this._startDownloads();\n }\n }\n }\n\n private async _downloadOne(\n id: string,\n signal: AbortSignal\n ): Promise<DownloadResult | null> {\n if (signal.aborted) return null;\n\n const record = await this.getRecord(id);\n if (!record || signal.aborted) return null;\n\n this._downloads.set(id, {\n id,\n filename: record.filename,\n phase: 'downloading',\n });\n\n try {\n // Download from storage\n const data = await this._withTimeout(\n this.config.storage.downloadFile(record.filename),\n this.downloadTimeoutMs,\n `Download timeout: ${record.filename}`\n );\n\n if (signal.aborted) return null;\n\n // Save to local file\n const localPath = `${id.replace(/[^a-zA-Z0-9]/g, '_')}`;\n const localUri = this.getLocalUri(localPath);\n\n // Ensure directory exists\n const dir = localUri.substring(0, localUri.lastIndexOf('/'));\n await this.platform.fileSystem.makeDirectory(dir, { intermediates: true });\n\n // Write file\n const content = data instanceof Blob\n ? await this._blobToBase64(data)\n : data;\n await this.platform.fileSystem.writeFile(localUri, content, 'base64');\n\n // Compress if applicable\n await this._compressImage(localUri, id, record.filename);\n\n // Get final size\n const info = await this.platform.fileSystem.getFileInfo(localUri);\n if (info && info.exists) {\n // Update record\n await this.powersync.execute(\n `UPDATE ${this.tableName}\n SET state = ?, local_uri = ?, size = ?\n WHERE id = ?`,\n [AttachmentState.SYNCED, localPath, info.size, id]\n );\n\n return { id, size: info.size };\n }\n\n return null;\n } catch (err) {\n this.logger.warn(`[AttachmentQueue] Failed: ${record.filename}`, err);\n return null;\n } finally {\n this._downloads.delete(id);\n this._notify();\n }\n }\n\n private async _batchUpdateSizes(results: DownloadResult[]): Promise<void> {\n if (results.length === 0) return;\n\n for (const { id, size } of results) {\n await this.powersync.execute(\n `UPDATE ${this.tableName} SET size = ? WHERE id = ? AND state = ?`,\n [size, id, AttachmentState.SYNCED]\n );\n }\n\n this._cachedSizeTimestamp = 0;\n }\n\n private async _getIdsToDownload(): Promise<string[]> {\n const result = await this.powersync.getAll<IdRow>(\n `SELECT id FROM ${this.tableName}\n WHERE state IN (?, ?)`,\n [AttachmentState.QUEUED_DOWNLOAD, AttachmentState.QUEUED_SYNC]\n );\n return result.map((r) => r.id);\n }\n\n // ─── Eviction ──────────────────────────────────────────────────────────────\n\n private async _evictIfNeeded(): Promise<void> {\n const evictionTrigger = this._maxCacheSize * this.evictionTriggerThreshold;\n const targetSize = this._maxCacheSize * this.downloadStopThreshold;\n\n let currentSize = await this._getCachedSizeWithCache(true);\n\n if (currentSize <= evictionTrigger) return;\n\n this.logger.info('[AttachmentQueue] Starting eviction...');\n\n while (currentSize > targetSize) {\n const toEvict = await this.powersync.getAll<EvictRow>(\n `SELECT id, local_uri, size FROM ${this.tableName}\n WHERE state = ${AttachmentState.SYNCED} AND size > 0 LIMIT 100`\n );\n\n if (toEvict.length === 0) break;\n\n for (const row of toEvict) {\n if (currentSize <= targetSize) break;\n\n if (row.local_uri) {\n try {\n await this.platform.fileSystem.deleteFile(this.getLocalUri(row.local_uri));\n await this.powersync.execute(\n `UPDATE ${this.tableName}\n SET state = ?, local_uri = NULL, size = 0 WHERE id = ?`,\n [AttachmentState.QUEUED_DOWNLOAD, row.id]\n );\n currentSize -= row.size;\n } catch {\n // Skip failed deletions\n }\n }\n }\n\n this._cachedSizeTimestamp = 0;\n currentSize = await this._getCachedSizeWithCache(true);\n }\n\n this._cacheFull = currentSize > targetSize;\n this._notify(true);\n\n if (!this._cacheFull && !this._paused && !this._processing) {\n this._startDownloads();\n }\n }\n\n // ─── Compression ───────────────────────────────────────────────────────────\n\n private async _compressImage(\n localUri: string,\n id: string,\n filename: string\n ): Promise<void> {\n if (!this._isImage(filename)) return;\n if (!this.platform.imageProcessor) return;\n\n const compressionConfig = { ...DEFAULT_COMPRESSION_CONFIG, ...this.config.compression };\n if (!compressionConfig.enabled) return;\n\n try {\n const info = await this.platform.fileSystem.getFileInfo(localUri);\n if (!info || !info.exists) return;\n\n const originalSize = info.size;\n\n if (originalSize < compressionConfig.skipSizeBytes) return;\n if (originalSize < compressionConfig.targetSizeBytes) return;\n\n // Update phase\n const existing = this._downloads.get(id);\n if (existing) {\n this._downloads.set(id, { ...existing, phase: 'compressing' });\n this._notify(false);\n }\n\n const result = await this.platform.imageProcessor.compress(localUri, {\n quality: this._compressionQuality,\n maxWidth: originalSize > 1_000_000 ? compressionConfig.maxWidth : undefined,\n format: 'jpeg',\n });\n\n const compressedInfo = await this.platform.fileSystem.getFileInfo(result.uri);\n if (!compressedInfo || !compressedInfo.exists) return;\n\n if (compressedInfo.size >= originalSize) {\n await this.platform.fileSystem.deleteFile(result.uri);\n return;\n }\n\n // Replace original with compressed\n await this.platform.fileSystem.deleteFile(localUri);\n await this.platform.fileSystem.moveFile(result.uri, localUri);\n\n this.logger.info(\n `[AttachmentQueue] Compressed ${filename}: ${Math.round(originalSize / 1024)}KB → ${Math.round(compressedInfo.size / 1024)}KB`\n );\n } catch (err) {\n this.logger.warn(`[AttachmentQueue] Compression failed for ${filename}:`, err);\n }\n }\n\n // ─── Helpers ───────────────────────────────────────────────────────────────\n\n private _isImage(filename: string): boolean {\n const ext = filename.split('.').pop()?.toLowerCase();\n return ['jpg', 'jpeg', 'png', 'webp', 'heic', 'heif'].includes(ext ?? '');\n }\n\n private _getStatus(pendingCount: number): AttachmentSyncStatus {\n if (this._processing) return 'syncing';\n if (this._paused) return 'paused';\n if (this._cacheFull && pendingCount > 0) return 'cache_full';\n return 'complete';\n }\n\n private async _getCachedSizeWithCache(forceRefresh = false): Promise<number> {\n const now = Date.now();\n\n if (!forceRefresh && this._cachedSizeTimestamp > 0 && now - this._cachedSizeTimestamp < CACHE_SIZE_TTL_MS) {\n return this._cachedSize;\n }\n\n const result = await this.powersync.get<CachedSizeRow>(\n `SELECT COALESCE(SUM(size), 0) as total FROM ${this.tableName}\n WHERE state = ${AttachmentState.SYNCED}`\n );\n\n this._cachedSize = result?.total ?? 0;\n this._cachedSizeTimestamp = now;\n return this._cachedSize;\n }\n\n private async _orderByNewest(ids: string[]): Promise<string[]> {\n if (ids.length <= 3) return ids;\n\n const { table, idColumn, orderByColumn } = this.config.source;\n if (!orderByColumn) return ids;\n\n try {\n const idSet = new Set(ids);\n const ordered = await this.powersync.getAll<{ id: string }>(\n `SELECT ${idColumn} as id FROM ${table}\n WHERE ${idColumn} IS NOT NULL AND ${idColumn} != ''\n ORDER BY ${orderByColumn} DESC NULLS LAST\n LIMIT ?`,\n [ids.length * 2]\n );\n\n const result: string[] = [];\n for (const row of ordered) {\n if (idSet.has(row.id)) {\n result.push(row.id);\n idSet.delete(row.id);\n }\n }\n\n for (const id of ids) {\n if (idSet.has(id)) {\n result.push(id);\n }\n }\n\n return result;\n } catch {\n return ids;\n }\n }\n\n private _sleep(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal.addEventListener('abort', onAbort, { once: true });\n });\n }\n\n private _withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(message)), ms);\n promise.then(\n (result) => {\n clearTimeout(timer);\n resolve(result);\n },\n (err) => {\n clearTimeout(timer);\n reject(err);\n }\n );\n });\n }\n\n private async _blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onloadend = () => {\n const base64 = (reader.result as string).split(',')[1];\n resolve(base64);\n };\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n }\n\n private _notify(forceImmediate = false): void {\n if (this._progressCallbacks.size === 0) return;\n\n const now = Date.now();\n const timeSinceLastNotify = now - this._lastNotifyTime;\n\n if (this._notifyTimer) {\n clearTimeout(this._notifyTimer);\n this._notifyTimer = null;\n }\n\n const notifyAll = (stats: AttachmentSyncStats) => {\n for (const cb of this._progressCallbacks) {\n try {\n cb(stats);\n } catch (err) {\n this.logger.warn('[AttachmentQueue] Callback error:', err);\n }\n }\n };\n\n if (forceImmediate || timeSinceLastNotify >= NOTIFY_THROTTLE_MS) {\n this._lastNotifyTime = now;\n this.getStats().then(notifyAll).catch(() => {});\n } else {\n const delay = NOTIFY_THROTTLE_MS - timeSinceLastNotify;\n this._notifyTimer = setTimeout(() => {\n this._notifyTimer = null;\n this._lastNotifyTime = Date.now();\n this.getStats().then(notifyAll).catch(() => {});\n }, delay);\n }\n }\n}\n"],"mappings":"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","names":["AttachmentState"]}
|