local-lite 0.0.21 → 0.0.22
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/adapters/remote/convex.d.ts +7 -0
- package/dist/adapters/remote/convex.js +10 -1
- package/dist/adapters/remote/firestore.d.ts +7 -0
- package/dist/adapters/remote/firestore.js +23 -1
- package/dist/adapters/remote/supabase.d.ts +7 -0
- package/dist/adapters/remote/supabase.js +13 -2
- package/dist/core/client.js +12 -0
- package/dist/storage/convex.d.ts +43 -0
- package/dist/storage/convex.js +112 -0
- package/dist/storage/expo.d.ts +132 -0
- package/dist/storage/expo.js +408 -0
- package/dist/storage/factory.d.ts +16 -0
- package/dist/storage/factory.js +22 -0
- package/dist/storage/firebase.d.ts +44 -0
- package/dist/storage/firebase.js +135 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/manager.d.ts +20 -0
- package/dist/storage/manager.js +144 -0
- package/dist/storage/supabase.d.ts +44 -0
- package/dist/storage/supabase.js +83 -0
- package/dist/storage/types.d.ts +29 -3
- package/package.json +6 -2
package/dist/storage/manager.js
CHANGED
|
@@ -12,12 +12,14 @@ class BlobStorageManager {
|
|
|
12
12
|
uploader;
|
|
13
13
|
network;
|
|
14
14
|
tableName;
|
|
15
|
+
deleteRemoteOnDelete;
|
|
15
16
|
constructor(db, options = {}) {
|
|
16
17
|
this.db = db;
|
|
17
18
|
this.driver = options.driver ?? new driver_js_1.MemoryFileDriver();
|
|
18
19
|
this.uploader = options.uploader;
|
|
19
20
|
this.network = options.network;
|
|
20
21
|
this.tableName = options.tableName ?? store_js_1.DEFAULT_BLOBS_TABLE;
|
|
22
|
+
this.deleteRemoteOnDelete = options.deleteRemoteOnDelete ?? false;
|
|
21
23
|
}
|
|
22
24
|
async initialize() {
|
|
23
25
|
await store_js_1.BlobStore.ensureSchema(this.db, this.tableName);
|
|
@@ -109,6 +111,148 @@ class BlobStorageManager {
|
|
|
109
111
|
async delete(key) {
|
|
110
112
|
await this.driver.delete(key);
|
|
111
113
|
await store_js_1.BlobStore.deleteByKey(this.db, this.tableName, key);
|
|
114
|
+
if (this.deleteRemoteOnDelete && this.uploader && typeof this.uploader.delete === 'function') {
|
|
115
|
+
try {
|
|
116
|
+
await this.uploader.delete(key);
|
|
117
|
+
}
|
|
118
|
+
catch { }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Retrieves the remote download/serving URL for a stored blob key.
|
|
123
|
+
*/
|
|
124
|
+
async getDownloadUrl(key) {
|
|
125
|
+
const record = await store_js_1.BlobStore.getByKey(this.db, this.tableName, key);
|
|
126
|
+
if (record?.remoteUrl)
|
|
127
|
+
return record.remoteUrl;
|
|
128
|
+
if (this.uploader && typeof this.uploader.getDownloadUrl === 'function') {
|
|
129
|
+
try {
|
|
130
|
+
return await this.uploader.getDownloadUrl(key);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Retrieves the local file path or URI (e.g. file:///... on mobile) for a stored blob key.
|
|
140
|
+
* Ideal for React Native <Image source={{ uri }} /> without loading bytes into JS memory.
|
|
141
|
+
*/
|
|
142
|
+
async getLocalUri(key) {
|
|
143
|
+
const record = await store_js_1.BlobStore.getByKey(this.db, this.tableName, key);
|
|
144
|
+
if (!record)
|
|
145
|
+
return null;
|
|
146
|
+
if (typeof this.driver.getUri === 'function') {
|
|
147
|
+
return await this.driver.getUri(key);
|
|
148
|
+
}
|
|
149
|
+
return record.localPath;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Directly imports a file from a local filesystem URI (e.g. expo-image-picker or expo-document-picker)
|
|
153
|
+
* into local-lite managed storage without having to manually read and encode byte arrays in memory.
|
|
154
|
+
*/
|
|
155
|
+
async saveFromUri(key, sourceUri, options = {}) {
|
|
156
|
+
let localPath;
|
|
157
|
+
let size = 0;
|
|
158
|
+
let bytes = null;
|
|
159
|
+
if (typeof this.driver.copyFrom === 'function') {
|
|
160
|
+
localPath = await this.driver.copyFrom(sourceUri, key);
|
|
161
|
+
if (typeof this.driver.getSize === 'function') {
|
|
162
|
+
size = await this.driver.getSize(key);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const res = await fetch(sourceUri);
|
|
167
|
+
const ab = await res.arrayBuffer();
|
|
168
|
+
bytes = new Uint8Array(ab);
|
|
169
|
+
size = bytes.byteLength;
|
|
170
|
+
localPath = await this.driver.write(key, bytes);
|
|
171
|
+
}
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const existing = await store_js_1.BlobStore.getByKey(this.db, this.tableName, key);
|
|
174
|
+
const id = existing ? existing.id : (0, uuid_js_1.generateUUID)();
|
|
175
|
+
const record = {
|
|
176
|
+
id,
|
|
177
|
+
key,
|
|
178
|
+
name: options.name || key.split('/').pop() || key,
|
|
179
|
+
mimeType: options.mimeType || 'application/octet-stream',
|
|
180
|
+
size,
|
|
181
|
+
localPath,
|
|
182
|
+
remoteUrl: existing?.remoteUrl || null,
|
|
183
|
+
status: 'PENDING',
|
|
184
|
+
metadata: options.metadata || existing?.metadata,
|
|
185
|
+
createdAt: existing ? existing.createdAt : now,
|
|
186
|
+
updatedAt: now,
|
|
187
|
+
};
|
|
188
|
+
await store_js_1.BlobStore.insertOrUpdate(this.db, this.tableName, record);
|
|
189
|
+
if (options.autoUpload !== false && this.uploader) {
|
|
190
|
+
const isOnline = this.network ? await this.network.isOnline() : true;
|
|
191
|
+
if (isOnline) {
|
|
192
|
+
try {
|
|
193
|
+
if (!bytes) {
|
|
194
|
+
bytes = await this.driver.read(key);
|
|
195
|
+
}
|
|
196
|
+
if (bytes) {
|
|
197
|
+
await store_js_1.BlobStore.updateStatus(this.db, this.tableName, id, 'UPLOADING');
|
|
198
|
+
const res = await this.uploader.upload(record, bytes);
|
|
199
|
+
record.status = 'UPLOADED';
|
|
200
|
+
record.remoteUrl = res.remoteUrl;
|
|
201
|
+
await store_js_1.BlobStore.updateStatus(this.db, this.tableName, id, 'UPLOADED', res.remoteUrl);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
record.status = 'FAILED';
|
|
206
|
+
await store_js_1.BlobStore.updateStatus(this.db, this.tableName, id, 'FAILED');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return record;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Downloads a remote asset (e.g. from Supabase Storage) and saves it to local disk.
|
|
214
|
+
* Caches metadata with status 'UPLOADED'.
|
|
215
|
+
*/
|
|
216
|
+
async download(key, remoteUrl, options = {}) {
|
|
217
|
+
let localPath;
|
|
218
|
+
let size = 0;
|
|
219
|
+
let hash = null;
|
|
220
|
+
if (typeof this.driver.downloadFrom === 'function') {
|
|
221
|
+
localPath = await this.driver.downloadFrom(remoteUrl, key);
|
|
222
|
+
if (typeof this.driver.getSize === 'function') {
|
|
223
|
+
size = await this.driver.getSize(key);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const res = await fetch(remoteUrl);
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
throw new Error(`Failed to download blob from "${remoteUrl}": ${res.statusText}`);
|
|
230
|
+
}
|
|
231
|
+
const ab = await res.arrayBuffer();
|
|
232
|
+
const bytes = new Uint8Array(ab);
|
|
233
|
+
size = bytes.byteLength;
|
|
234
|
+
hash = (0, utils_js_1.bytesToHex)((0, sha2_js_1.sha256)(bytes));
|
|
235
|
+
localPath = await this.driver.write(key, bytes);
|
|
236
|
+
}
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
const existing = await store_js_1.BlobStore.getByKey(this.db, this.tableName, key);
|
|
239
|
+
const id = existing ? existing.id : (0, uuid_js_1.generateUUID)();
|
|
240
|
+
const record = {
|
|
241
|
+
id,
|
|
242
|
+
key,
|
|
243
|
+
name: options.name || key.split('/').pop() || key,
|
|
244
|
+
mimeType: options.mimeType || 'application/octet-stream',
|
|
245
|
+
size,
|
|
246
|
+
localPath,
|
|
247
|
+
remoteUrl,
|
|
248
|
+
status: 'UPLOADED',
|
|
249
|
+
sha256: hash,
|
|
250
|
+
metadata: options.metadata || existing?.metadata,
|
|
251
|
+
createdAt: existing ? existing.createdAt : now,
|
|
252
|
+
updatedAt: now,
|
|
253
|
+
};
|
|
254
|
+
await store_js_1.BlobStore.insertOrUpdate(this.db, this.tableName, record);
|
|
255
|
+
return record;
|
|
112
256
|
}
|
|
113
257
|
/**
|
|
114
258
|
* Flushes and syncs pending or failed uploads to the remote storage uploader.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import { BlobRecord, RemoteStorageUploader } from './types.js';
|
|
3
|
+
export interface SupabaseStorageUploaderOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Supabase Storage bucket name (e.g. 'avatars', 'property-photos', 'documents').
|
|
6
|
+
*/
|
|
7
|
+
bucket: string;
|
|
8
|
+
/**
|
|
9
|
+
* Whether to overwrite existing files on Supabase with the same path/key.
|
|
10
|
+
* Default: true
|
|
11
|
+
*/
|
|
12
|
+
upsert?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Custom path transformation function from local blob key to Supabase Storage path.
|
|
15
|
+
* By default, returns `key.replace(/^\/+/, '')`.
|
|
16
|
+
*/
|
|
17
|
+
pathTransform?: (key: string, blob?: BlobRecord) => string;
|
|
18
|
+
/**
|
|
19
|
+
* URL format to store in metadata:
|
|
20
|
+
* - 'public': Uses `supabase.storage.from(bucket).getPublicUrl(path)`
|
|
21
|
+
* - 'signed': Uses `supabase.storage.from(bucket).createSignedUrl(path, expiresIn)`
|
|
22
|
+
* Default: 'public'
|
|
23
|
+
*/
|
|
24
|
+
urlType?: 'public' | 'signed';
|
|
25
|
+
/**
|
|
26
|
+
* Expiration duration in seconds when generating signed URLs.
|
|
27
|
+
* Default: 86400 (24 hours)
|
|
28
|
+
*/
|
|
29
|
+
signedUrlExpiresIn?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Cache control HTTP header for uploaded assets (e.g. '3600').
|
|
32
|
+
*/
|
|
33
|
+
cacheControl?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface SupabaseStorageUploaderInstance extends RemoteStorageUploader {
|
|
36
|
+
getPublicUrl(keyOrPath: string): string;
|
|
37
|
+
createSignedUrl(keyOrPath: string, expiresIn?: number): Promise<string>;
|
|
38
|
+
getDownloadUrl(keyOrPath: string): Promise<string> | string;
|
|
39
|
+
delete(keyOrPath: string): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Creates a RemoteStorageUploader that syncs binary files to Supabase Storage buckets.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createSupabaseStorageUploader(supabase: SupabaseClient | any, options: SupabaseStorageUploaderOptions): SupabaseStorageUploaderInstance;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSupabaseStorageUploader = createSupabaseStorageUploader;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a RemoteStorageUploader that syncs binary files to Supabase Storage buckets.
|
|
6
|
+
*/
|
|
7
|
+
function createSupabaseStorageUploader(supabase, options) {
|
|
8
|
+
const bucketName = options.bucket;
|
|
9
|
+
const getBucket = () => {
|
|
10
|
+
if (typeof supabase.storage?.from === 'function') {
|
|
11
|
+
return supabase.storage.from(bucketName);
|
|
12
|
+
}
|
|
13
|
+
if (typeof supabase.from === 'function') {
|
|
14
|
+
return supabase.from(bucketName);
|
|
15
|
+
}
|
|
16
|
+
throw new Error('Supabase client does not have storage.from() available.');
|
|
17
|
+
};
|
|
18
|
+
const resolveRemotePath = (key, blob) => {
|
|
19
|
+
if (options.pathTransform) {
|
|
20
|
+
return options.pathTransform(key, blob);
|
|
21
|
+
}
|
|
22
|
+
return key.replace(/^\/+/, '');
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
async upload(blob, data) {
|
|
26
|
+
const bucket = getBucket();
|
|
27
|
+
const remotePath = resolveRemotePath(blob.key, blob);
|
|
28
|
+
const uploadOptions = {
|
|
29
|
+
contentType: blob.mimeType || 'application/octet-stream',
|
|
30
|
+
upsert: options.upsert !== false,
|
|
31
|
+
};
|
|
32
|
+
if (options.cacheControl) {
|
|
33
|
+
uploadOptions.cacheControl = options.cacheControl;
|
|
34
|
+
}
|
|
35
|
+
// Supabase Storage accepts Uint8Array, Buffer, or ArrayBuffer
|
|
36
|
+
const { error } = await bucket.upload(remotePath, data, uploadOptions);
|
|
37
|
+
if (error) {
|
|
38
|
+
throw new Error(`Supabase Storage upload failed for "${remotePath}": ${error.message || JSON.stringify(error)}`);
|
|
39
|
+
}
|
|
40
|
+
if (options.urlType === 'signed') {
|
|
41
|
+
const expiresIn = options.signedUrlExpiresIn ?? 86400;
|
|
42
|
+
const { data: signedData, error: signedErr } = await bucket.createSignedUrl(remotePath, expiresIn);
|
|
43
|
+
if (signedErr || !signedData?.signedUrl) {
|
|
44
|
+
throw new Error(`Failed to create signed URL for "${remotePath}": ${signedErr?.message || 'unknown error'}`);
|
|
45
|
+
}
|
|
46
|
+
return { remoteUrl: signedData.signedUrl };
|
|
47
|
+
}
|
|
48
|
+
const { data: publicData } = bucket.getPublicUrl(remotePath);
|
|
49
|
+
return { remoteUrl: publicData.publicUrl };
|
|
50
|
+
},
|
|
51
|
+
getPublicUrl(keyOrPath) {
|
|
52
|
+
const bucket = getBucket();
|
|
53
|
+
const remotePath = resolveRemotePath(keyOrPath);
|
|
54
|
+
const { data } = bucket.getPublicUrl(remotePath);
|
|
55
|
+
return data.publicUrl;
|
|
56
|
+
},
|
|
57
|
+
async createSignedUrl(keyOrPath, expiresIn = 86400) {
|
|
58
|
+
const bucket = getBucket();
|
|
59
|
+
const remotePath = resolveRemotePath(keyOrPath);
|
|
60
|
+
const { data, error } = await bucket.createSignedUrl(remotePath, expiresIn);
|
|
61
|
+
if (error || !data?.signedUrl) {
|
|
62
|
+
throw new Error(`Failed to create signed URL for "${remotePath}": ${error?.message || 'unknown error'}`);
|
|
63
|
+
}
|
|
64
|
+
return data.signedUrl;
|
|
65
|
+
},
|
|
66
|
+
getDownloadUrl(keyOrPath) {
|
|
67
|
+
if (options.urlType === 'signed') {
|
|
68
|
+
return this.createSignedUrl(keyOrPath, options.signedUrlExpiresIn);
|
|
69
|
+
}
|
|
70
|
+
return this.getPublicUrl(keyOrPath);
|
|
71
|
+
},
|
|
72
|
+
async delete(keyOrPath) {
|
|
73
|
+
const bucket = getBucket();
|
|
74
|
+
const remotePath = resolveRemotePath(keyOrPath);
|
|
75
|
+
if (typeof bucket.remove === 'function') {
|
|
76
|
+
const { error } = await bucket.remove([remotePath]);
|
|
77
|
+
if (error) {
|
|
78
|
+
throw new Error(`Failed to delete "${remotePath}" from Supabase Storage: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -28,12 +28,22 @@ export interface FileDriver {
|
|
|
28
28
|
read(key: string): Promise<Uint8Array | null>;
|
|
29
29
|
delete(key: string): Promise<void>;
|
|
30
30
|
exists(key: string): Promise<boolean>;
|
|
31
|
+
getUri?(key: string): Promise<string> | string;
|
|
32
|
+
copyFrom?(sourceUri: string, key: string): Promise<string>;
|
|
33
|
+
downloadFrom?(remoteUrl: string, key: string): Promise<string>;
|
|
34
|
+
getSize?(key: string): Promise<number>;
|
|
35
|
+
}
|
|
36
|
+
export interface RemoteStorageUploadResult {
|
|
37
|
+
remoteUrl: string;
|
|
38
|
+
storageId?: string;
|
|
39
|
+
metadata?: Record<string, any>;
|
|
31
40
|
}
|
|
32
41
|
export interface RemoteStorageUploader {
|
|
33
|
-
upload(blob: BlobRecord, data: Uint8Array): Promise<
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
upload(blob: BlobRecord, data: Uint8Array): Promise<RemoteStorageUploadResult>;
|
|
43
|
+
getDownloadUrl?(keyOrPath: string): Promise<string> | string;
|
|
44
|
+
delete?(keyOrPath: string): Promise<void>;
|
|
36
45
|
}
|
|
46
|
+
export type StorageProviderType = 'supabase' | 'convex' | 'firebase';
|
|
37
47
|
export interface BlobStorageManagerOptions {
|
|
38
48
|
driver?: FileDriver;
|
|
39
49
|
uploader?: RemoteStorageUploader;
|
|
@@ -41,4 +51,20 @@ export interface BlobStorageManagerOptions {
|
|
|
41
51
|
isOnline(): boolean | Promise<boolean>;
|
|
42
52
|
};
|
|
43
53
|
tableName?: string;
|
|
54
|
+
/**
|
|
55
|
+
* If true, calling delete(key) on BlobStorageManager will also attempt to delete
|
|
56
|
+
* the asset from the remote storage uploader if supported. Default: false
|
|
57
|
+
*/
|
|
58
|
+
deleteRemoteOnDelete?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* If true, automatically attempts to infer the storage uploader from the client's
|
|
61
|
+
* remote database adapter (Supabase, Convex, or Firebase) if bucket/mutation options are provided.
|
|
62
|
+
*/
|
|
63
|
+
autoRemote?: boolean;
|
|
64
|
+
/** Optional bucket name for Supabase or Firebase Storage */
|
|
65
|
+
bucket?: string;
|
|
66
|
+
/** Optional mutation reference for Convex Storage */
|
|
67
|
+
uploadMutation?: any;
|
|
68
|
+
/** Additional provider-specific configuration */
|
|
69
|
+
[key: string]: any;
|
|
44
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-lite",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "Never let a lost connection break a promise. Local-first sync that faithfully keeps data safe until you're back online.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -144,7 +144,8 @@
|
|
|
144
144
|
"@supabase/supabase-js": "^2.0.0",
|
|
145
145
|
"better-sqlite3": "^8.0.0",
|
|
146
146
|
"convex": ">=1.0.0",
|
|
147
|
-
"expo-
|
|
147
|
+
"expo-file-system": ">=58.0.0",
|
|
148
|
+
"expo-sqlite": ">=58.0.0",
|
|
148
149
|
"firebase": ">=9.0.0",
|
|
149
150
|
"react": ">=18.0.0",
|
|
150
151
|
"sql.js": "^1.8.0"
|
|
@@ -162,6 +163,9 @@
|
|
|
162
163
|
"firebase": {
|
|
163
164
|
"optional": true
|
|
164
165
|
},
|
|
166
|
+
"expo-file-system": {
|
|
167
|
+
"optional": true
|
|
168
|
+
},
|
|
165
169
|
"expo-sqlite": {
|
|
166
170
|
"optional": true
|
|
167
171
|
},
|