silosdk 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/dist/internal/indexed-db.cjs +44 -0
- package/dist/internal/indexed-db.mjs +41 -0
- package/dist/media/shared.cjs +99 -0
- package/dist/media/shared.d.cts +26 -0
- package/dist/media/shared.d.mts +26 -0
- package/dist/media/shared.mjs +93 -0
- package/dist/media/storage.native.cjs +102 -0
- package/dist/media/storage.native.mjs +101 -0
- package/dist/media/storage.web.cjs +79 -0
- package/dist/media/storage.web.mjs +78 -0
- package/dist/media.cjs +5 -131
- package/dist/media.d.cts +8 -25
- package/dist/media.d.mts +8 -25
- package/dist/media.mjs +4 -129
- package/dist/media.web.cjs +9 -0
- package/dist/media.web.d.cts +12 -0
- package/dist/media.web.d.mts +12 -0
- package/dist/media.web.mjs +8 -0
- package/dist/settings/shared.cjs +63 -0
- package/dist/settings/shared.d.cts +19 -0
- package/dist/settings/shared.d.mts +19 -0
- package/dist/settings/shared.mjs +59 -0
- package/dist/settings/storage.native.cjs +24 -0
- package/dist/settings/storage.native.mjs +22 -0
- package/dist/settings/storage.web.cjs +34 -0
- package/dist/settings/storage.web.mjs +34 -0
- package/dist/settings.cjs +4 -66
- package/dist/settings.d.cts +4 -18
- package/dist/settings.d.mts +4 -18
- package/dist/settings.mjs +4 -64
- package/dist/settings.web.cjs +8 -0
- package/dist/settings.web.d.cts +6 -0
- package/dist/settings.web.d.mts +6 -0
- package/dist/settings.web.mjs +8 -0
- package/dist/store/shared.cjs +338 -0
- package/dist/store/shared.d.cts +58 -0
- package/dist/store/shared.d.mts +58 -0
- package/dist/store/shared.mjs +333 -0
- package/dist/store/storage.native.cjs +133 -0
- package/dist/store/storage.native.mjs +132 -0
- package/dist/store/storage.web.cjs +142 -0
- package/dist/store/storage.web.mjs +142 -0
- package/dist/store.cjs +4 -421
- package/dist/store.d.cts +4 -58
- package/dist/store.d.mts +4 -58
- package/dist/store.mjs +4 -421
- package/dist/store.web.cjs +15 -0
- package/dist/store.web.d.cts +7 -0
- package/dist/store.web.d.mts +7 -0
- package/dist/store.web.mjs +12 -0
- package/package.json +31 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ TanStack hooks.
|
|
|
6
6
|
|
|
7
7
|
Silo owns:
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- local platform persistence
|
|
10
10
|
- flat primitive document constraints
|
|
11
11
|
- defaults-first source shape
|
|
12
12
|
- settings storage
|
|
@@ -25,10 +25,16 @@ TanStack owns:
|
|
|
25
25
|
yarn add silosdk @tanstack/react-db @tanstack/react-query expo-crypto expo-sqlite expo-file-system
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
Native apps use Expo SQLite, Expo FileSystem, and Expo SQLite KV storage. Web
|
|
29
|
+
builds use IndexedDB for stores, settings, and media blobs through browser export
|
|
30
|
+
entrypoints, so the web bundle does not need to load the native SQLite or
|
|
31
|
+
filesystem storage modules.
|
|
32
|
+
|
|
28
33
|
## Stores
|
|
29
34
|
|
|
30
|
-
Stores define
|
|
31
|
-
relationships between them.
|
|
35
|
+
Stores define locally persisted document collections and a link collection for
|
|
36
|
+
relationships between them. Native storage uses SQLite; web storage uses
|
|
37
|
+
IndexedDB. Stores produce options for TanStack DB collections.
|
|
32
38
|
|
|
33
39
|
```ts
|
|
34
40
|
import { createCollection } from '@tanstack/react-db'
|
|
@@ -154,8 +160,9 @@ only need one collection definition.
|
|
|
154
160
|
|
|
155
161
|
## Settings
|
|
156
162
|
|
|
157
|
-
Settings are small persisted primitive values
|
|
158
|
-
|
|
163
|
+
Settings are small persisted primitive values. Native storage uses
|
|
164
|
+
`expo-sqlite/kv-store`; web storage uses IndexedDB. Settings produce TanStack
|
|
165
|
+
Query options.
|
|
159
166
|
|
|
160
167
|
```ts
|
|
161
168
|
import { setting } from 'silosdk/settings'
|
|
@@ -186,6 +193,10 @@ type SettingValue = string | number | boolean | null
|
|
|
186
193
|
Media is singleton infrastructure for app-owned files. Domain documents store
|
|
187
194
|
stable media refs as plain strings, keeping source rows flat.
|
|
188
195
|
|
|
196
|
+
Native media imports copy files into Silo-owned document storage and persist the
|
|
197
|
+
mapping in SQLite. Web media imports store metadata and blobs in IndexedDB and
|
|
198
|
+
resolve refs to cached `blob:` URLs.
|
|
199
|
+
|
|
189
200
|
```ts
|
|
190
201
|
import { media, type MediaRef } from 'silosdk/media'
|
|
191
202
|
```
|
|
@@ -223,6 +234,18 @@ const { data: uri } = useQuery(media.queryOptions(post.coverImage))
|
|
|
223
234
|
`media.queryOptions()` returns `string | null`. It returns `null` when the ref is
|
|
224
235
|
empty, invalid, unknown, or missing on disk.
|
|
225
236
|
|
|
237
|
+
Delete a media ref when your app no longer stores it:
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
const deleteMedia = useMutation(media.deleteMutationOptions({ queryClient }))
|
|
241
|
+
|
|
242
|
+
await deleteMedia.mutateAsync(coverImage)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Deletion removes Silo's copied file and media mapping only. Remove the ref from
|
|
246
|
+
your own source rows or settings separately. Invalid or unknown refs are safe and
|
|
247
|
+
return `false`.
|
|
248
|
+
|
|
226
249
|
## API Shape
|
|
227
250
|
|
|
228
251
|
Silo exposes definition objects and option factories:
|
|
@@ -234,6 +257,7 @@ useQuery(theme.queryOptions())
|
|
|
234
257
|
useMutation(theme.mutationOptions({ queryClient }))
|
|
235
258
|
useQuery(media.queryOptions(ref))
|
|
236
259
|
useMutation(media.mutationOptions({ queryClient }))
|
|
260
|
+
useMutation(media.deleteMutationOptions({ queryClient }))
|
|
237
261
|
```
|
|
238
262
|
|
|
239
263
|
There is no Silo provider and no Silo hook wrapper API.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/internal/indexed-db.ts
|
|
3
|
+
function createIndexedDbOpener(name, version, upgrade) {
|
|
4
|
+
let promise;
|
|
5
|
+
return () => {
|
|
6
|
+
if (promise) return promise;
|
|
7
|
+
promise = new Promise((resolve, reject) => {
|
|
8
|
+
const request = indexedDB.open(name, version);
|
|
9
|
+
request.onupgradeneeded = () => upgrade(request.result);
|
|
10
|
+
request.onsuccess = () => {
|
|
11
|
+
const database = request.result;
|
|
12
|
+
database.onversionchange = () => database.close();
|
|
13
|
+
resolve(database);
|
|
14
|
+
};
|
|
15
|
+
request.onerror = () => {
|
|
16
|
+
promise = void 0;
|
|
17
|
+
reject(request.error);
|
|
18
|
+
};
|
|
19
|
+
request.onblocked = () => {
|
|
20
|
+
promise = void 0;
|
|
21
|
+
reject(/* @__PURE__ */ new Error(`IndexedDB database "${name}" upgrade was blocked.`));
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return promise;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function requestResult(request) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
request.onsuccess = () => resolve(request.result);
|
|
30
|
+
request.onerror = () => reject(request.error);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function transactionDone(transaction) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
transaction.oncomplete = () => resolve();
|
|
36
|
+
transaction.onerror = () => reject(transaction.error);
|
|
37
|
+
transaction.onabort = () => reject(transaction.error);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
exports.createIndexedDbOpener = createIndexedDbOpener;
|
|
43
|
+
exports.requestResult = requestResult;
|
|
44
|
+
exports.transactionDone = transactionDone;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/internal/indexed-db.ts
|
|
2
|
+
function createIndexedDbOpener(name, version, upgrade) {
|
|
3
|
+
let promise;
|
|
4
|
+
return () => {
|
|
5
|
+
if (promise) return promise;
|
|
6
|
+
promise = new Promise((resolve, reject) => {
|
|
7
|
+
const request = indexedDB.open(name, version);
|
|
8
|
+
request.onupgradeneeded = () => upgrade(request.result);
|
|
9
|
+
request.onsuccess = () => {
|
|
10
|
+
const database = request.result;
|
|
11
|
+
database.onversionchange = () => database.close();
|
|
12
|
+
resolve(database);
|
|
13
|
+
};
|
|
14
|
+
request.onerror = () => {
|
|
15
|
+
promise = void 0;
|
|
16
|
+
reject(request.error);
|
|
17
|
+
};
|
|
18
|
+
request.onblocked = () => {
|
|
19
|
+
promise = void 0;
|
|
20
|
+
reject(/* @__PURE__ */ new Error(`IndexedDB database "${name}" upgrade was blocked.`));
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
return promise;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function requestResult(request) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
request.onsuccess = () => resolve(request.result);
|
|
29
|
+
request.onerror = () => reject(request.error);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function transactionDone(transaction) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
transaction.oncomplete = () => resolve();
|
|
35
|
+
transaction.onerror = () => reject(transaction.error);
|
|
36
|
+
transaction.onabort = () => reject(transaction.error);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { createIndexedDbOpener, requestResult, transactionDone };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
let __tanstack_react_query = require("@tanstack/react-query");
|
|
3
|
+
|
|
4
|
+
//#region src/media/shared.ts
|
|
5
|
+
function createMediaApi(storage) {
|
|
6
|
+
return {
|
|
7
|
+
queryOptions(ref) {
|
|
8
|
+
return (0, __tanstack_react_query.queryOptions)({
|
|
9
|
+
queryKey: mediaQueryKey(ref),
|
|
10
|
+
queryFn: () => resolveMediaUri(storage, ref)
|
|
11
|
+
});
|
|
12
|
+
},
|
|
13
|
+
mutationOptions(options = {}) {
|
|
14
|
+
return (0, __tanstack_react_query.mutationOptions)({
|
|
15
|
+
mutationKey: [
|
|
16
|
+
"silo",
|
|
17
|
+
"media",
|
|
18
|
+
"import"
|
|
19
|
+
],
|
|
20
|
+
mutationFn: async (input) => {
|
|
21
|
+
assertMediaKind(input.kind);
|
|
22
|
+
const imported = await storage.importMedia(input);
|
|
23
|
+
options.queryClient?.setQueryData(mediaQueryKey(imported.ref), imported.uri);
|
|
24
|
+
return imported.ref;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
delete(ref) {
|
|
29
|
+
return deleteMedia(storage, ref);
|
|
30
|
+
},
|
|
31
|
+
deleteMutationOptions(options = {}) {
|
|
32
|
+
return (0, __tanstack_react_query.mutationOptions)({
|
|
33
|
+
mutationKey: [
|
|
34
|
+
"silo",
|
|
35
|
+
"media",
|
|
36
|
+
"delete"
|
|
37
|
+
],
|
|
38
|
+
mutationFn: async (ref) => {
|
|
39
|
+
const deleted = await deleteMedia(storage, ref);
|
|
40
|
+
if (isMediaRef(ref)) options.queryClient?.setQueryData(mediaQueryKey(ref), null);
|
|
41
|
+
return deleted;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function isMediaRef(value) {
|
|
48
|
+
return typeof value === "string" && value.startsWith("media://");
|
|
49
|
+
}
|
|
50
|
+
function createMediaRef(kind, id) {
|
|
51
|
+
return `media://silo/${kindDirectoryName(kind)}/${id}`;
|
|
52
|
+
}
|
|
53
|
+
function kindDirectoryName(kind) {
|
|
54
|
+
switch (kind) {
|
|
55
|
+
case "image": return "images";
|
|
56
|
+
case "video": return "videos";
|
|
57
|
+
case "audio": return "audio";
|
|
58
|
+
case "file": return "files";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function extensionFor(name) {
|
|
62
|
+
return name.match(/\.[A-Za-z0-9]+$/)?.[0] ?? "";
|
|
63
|
+
}
|
|
64
|
+
function extensionForMimeType(mimeType) {
|
|
65
|
+
switch (mimeType) {
|
|
66
|
+
case "application/pdf": return ".pdf";
|
|
67
|
+
case "image/jpeg": return ".jpg";
|
|
68
|
+
case "image/png": return ".png";
|
|
69
|
+
case "image/webp": return ".webp";
|
|
70
|
+
case "image/gif": return ".gif";
|
|
71
|
+
default: return "";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function mediaQueryKey(ref) {
|
|
75
|
+
return [
|
|
76
|
+
"silo",
|
|
77
|
+
"media",
|
|
78
|
+
ref ?? null
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
async function resolveMediaUri(storage, ref) {
|
|
82
|
+
if (!isMediaRef(ref)) return null;
|
|
83
|
+
return storage.resolveMediaUri(ref);
|
|
84
|
+
}
|
|
85
|
+
async function deleteMedia(storage, ref) {
|
|
86
|
+
if (!isMediaRef(ref)) return false;
|
|
87
|
+
return storage.deleteMedia(ref);
|
|
88
|
+
}
|
|
89
|
+
function assertMediaKind(kind) {
|
|
90
|
+
if (kind !== "image" && kind !== "video" && kind !== "audio" && kind !== "file") throw new Error("Media kind must be one of \"image\", \"video\", \"audio\", or \"file\".");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
exports.createMediaApi = createMediaApi;
|
|
95
|
+
exports.createMediaRef = createMediaRef;
|
|
96
|
+
exports.extensionFor = extensionFor;
|
|
97
|
+
exports.extensionForMimeType = extensionForMimeType;
|
|
98
|
+
exports.isMediaRef = isMediaRef;
|
|
99
|
+
exports.kindDirectoryName = kindDirectoryName;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { QueryClient, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
//#region src/media/shared.d.ts
|
|
4
|
+
type MediaRef = `media://${string}`;
|
|
5
|
+
type MediaKind = 'image' | 'video' | 'audio' | 'file';
|
|
6
|
+
type MediaJson = string | number | boolean | null | MediaJson[] | {
|
|
7
|
+
[key: string]: MediaJson;
|
|
8
|
+
};
|
|
9
|
+
type MediaData = {
|
|
10
|
+
[key: string]: MediaJson;
|
|
11
|
+
};
|
|
12
|
+
type MediaImportInput = {
|
|
13
|
+
uri: string;
|
|
14
|
+
kind: MediaKind;
|
|
15
|
+
name?: string;
|
|
16
|
+
mimeType?: string | null;
|
|
17
|
+
data?: MediaData;
|
|
18
|
+
};
|
|
19
|
+
type MediaDeleteInput = MediaRef | string | null | undefined;
|
|
20
|
+
type MediaQueryKey = readonly ['silo', 'media', MediaRef | string | null];
|
|
21
|
+
type MediaMutationOptions = {
|
|
22
|
+
queryClient?: QueryClient;
|
|
23
|
+
};
|
|
24
|
+
declare function isMediaRef(value: unknown): value is MediaRef;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { MediaData, MediaDeleteInput, MediaImportInput, MediaJson, MediaKind, MediaMutationOptions, MediaQueryKey, MediaRef, isMediaRef };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { QueryClient, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
//#region src/media/shared.d.ts
|
|
4
|
+
type MediaRef = `media://${string}`;
|
|
5
|
+
type MediaKind = 'image' | 'video' | 'audio' | 'file';
|
|
6
|
+
type MediaJson = string | number | boolean | null | MediaJson[] | {
|
|
7
|
+
[key: string]: MediaJson;
|
|
8
|
+
};
|
|
9
|
+
type MediaData = {
|
|
10
|
+
[key: string]: MediaJson;
|
|
11
|
+
};
|
|
12
|
+
type MediaImportInput = {
|
|
13
|
+
uri: string;
|
|
14
|
+
kind: MediaKind;
|
|
15
|
+
name?: string;
|
|
16
|
+
mimeType?: string | null;
|
|
17
|
+
data?: MediaData;
|
|
18
|
+
};
|
|
19
|
+
type MediaDeleteInput = MediaRef | string | null | undefined;
|
|
20
|
+
type MediaQueryKey = readonly ['silo', 'media', MediaRef | string | null];
|
|
21
|
+
type MediaMutationOptions = {
|
|
22
|
+
queryClient?: QueryClient;
|
|
23
|
+
};
|
|
24
|
+
declare function isMediaRef(value: unknown): value is MediaRef;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { MediaData, MediaDeleteInput, MediaImportInput, MediaJson, MediaKind, MediaMutationOptions, MediaQueryKey, MediaRef, isMediaRef };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { mutationOptions, queryOptions } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
//#region src/media/shared.ts
|
|
4
|
+
function createMediaApi(storage) {
|
|
5
|
+
return {
|
|
6
|
+
queryOptions(ref) {
|
|
7
|
+
return queryOptions({
|
|
8
|
+
queryKey: mediaQueryKey(ref),
|
|
9
|
+
queryFn: () => resolveMediaUri(storage, ref)
|
|
10
|
+
});
|
|
11
|
+
},
|
|
12
|
+
mutationOptions(options = {}) {
|
|
13
|
+
return mutationOptions({
|
|
14
|
+
mutationKey: [
|
|
15
|
+
"silo",
|
|
16
|
+
"media",
|
|
17
|
+
"import"
|
|
18
|
+
],
|
|
19
|
+
mutationFn: async (input) => {
|
|
20
|
+
assertMediaKind(input.kind);
|
|
21
|
+
const imported = await storage.importMedia(input);
|
|
22
|
+
options.queryClient?.setQueryData(mediaQueryKey(imported.ref), imported.uri);
|
|
23
|
+
return imported.ref;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
delete(ref) {
|
|
28
|
+
return deleteMedia(storage, ref);
|
|
29
|
+
},
|
|
30
|
+
deleteMutationOptions(options = {}) {
|
|
31
|
+
return mutationOptions({
|
|
32
|
+
mutationKey: [
|
|
33
|
+
"silo",
|
|
34
|
+
"media",
|
|
35
|
+
"delete"
|
|
36
|
+
],
|
|
37
|
+
mutationFn: async (ref) => {
|
|
38
|
+
const deleted = await deleteMedia(storage, ref);
|
|
39
|
+
if (isMediaRef(ref)) options.queryClient?.setQueryData(mediaQueryKey(ref), null);
|
|
40
|
+
return deleted;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function isMediaRef(value) {
|
|
47
|
+
return typeof value === "string" && value.startsWith("media://");
|
|
48
|
+
}
|
|
49
|
+
function createMediaRef(kind, id) {
|
|
50
|
+
return `media://silo/${kindDirectoryName(kind)}/${id}`;
|
|
51
|
+
}
|
|
52
|
+
function kindDirectoryName(kind) {
|
|
53
|
+
switch (kind) {
|
|
54
|
+
case "image": return "images";
|
|
55
|
+
case "video": return "videos";
|
|
56
|
+
case "audio": return "audio";
|
|
57
|
+
case "file": return "files";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function extensionFor(name) {
|
|
61
|
+
return name.match(/\.[A-Za-z0-9]+$/)?.[0] ?? "";
|
|
62
|
+
}
|
|
63
|
+
function extensionForMimeType(mimeType) {
|
|
64
|
+
switch (mimeType) {
|
|
65
|
+
case "application/pdf": return ".pdf";
|
|
66
|
+
case "image/jpeg": return ".jpg";
|
|
67
|
+
case "image/png": return ".png";
|
|
68
|
+
case "image/webp": return ".webp";
|
|
69
|
+
case "image/gif": return ".gif";
|
|
70
|
+
default: return "";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function mediaQueryKey(ref) {
|
|
74
|
+
return [
|
|
75
|
+
"silo",
|
|
76
|
+
"media",
|
|
77
|
+
ref ?? null
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
async function resolveMediaUri(storage, ref) {
|
|
81
|
+
if (!isMediaRef(ref)) return null;
|
|
82
|
+
return storage.resolveMediaUri(ref);
|
|
83
|
+
}
|
|
84
|
+
async function deleteMedia(storage, ref) {
|
|
85
|
+
if (!isMediaRef(ref)) return false;
|
|
86
|
+
return storage.deleteMedia(ref);
|
|
87
|
+
}
|
|
88
|
+
function assertMediaKind(kind) {
|
|
89
|
+
if (kind !== "image" && kind !== "video" && kind !== "audio" && kind !== "file") throw new Error("Media kind must be one of \"image\", \"video\", \"audio\", or \"file\".");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
export { createMediaApi, createMediaRef, extensionFor, extensionForMimeType, isMediaRef, kindDirectoryName };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_shared = require('./shared.cjs');
|
|
3
|
+
let expo_crypto = require("expo-crypto");
|
|
4
|
+
let expo_sqlite = require("expo-sqlite");
|
|
5
|
+
let expo_file_system = require("expo-file-system");
|
|
6
|
+
|
|
7
|
+
//#region src/media/storage.native.ts
|
|
8
|
+
let database;
|
|
9
|
+
const mediaStorage = {
|
|
10
|
+
importMedia,
|
|
11
|
+
resolveMediaUri,
|
|
12
|
+
deleteMedia
|
|
13
|
+
};
|
|
14
|
+
async function importMedia(input) {
|
|
15
|
+
const id = (0, expo_crypto.randomUUID)();
|
|
16
|
+
const ref = require_shared.createMediaRef(input.kind, id);
|
|
17
|
+
const source = new expo_file_system.File(input.uri);
|
|
18
|
+
const name = input.name ?? source.name ?? `${id}${source.extension}`;
|
|
19
|
+
const destinationDirectory = mediaDirectory(input.kind);
|
|
20
|
+
destinationDirectory.create({
|
|
21
|
+
idempotent: true,
|
|
22
|
+
intermediates: true
|
|
23
|
+
});
|
|
24
|
+
const destination = new expo_file_system.File(destinationDirectory, `${id}${require_shared.extensionFor(name)}`);
|
|
25
|
+
await source.copy(destination);
|
|
26
|
+
const info = destination.info();
|
|
27
|
+
const mimeType = input.mimeType ?? (destination.type || null);
|
|
28
|
+
const size = info.size ?? destination.size;
|
|
29
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
30
|
+
insertMediaFile({
|
|
31
|
+
ref,
|
|
32
|
+
uri: destination.uri,
|
|
33
|
+
kind: input.kind,
|
|
34
|
+
name,
|
|
35
|
+
mimeType,
|
|
36
|
+
size,
|
|
37
|
+
createdAt: timestamp,
|
|
38
|
+
data: input.data ?? {}
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
ref,
|
|
42
|
+
uri: destination.uri
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function resolveMediaUri(ref) {
|
|
46
|
+
const row = getMediaFile(ref);
|
|
47
|
+
if (!row) return null;
|
|
48
|
+
return new expo_file_system.File(row.uri).exists ? row.uri : null;
|
|
49
|
+
}
|
|
50
|
+
async function deleteMedia(ref) {
|
|
51
|
+
const row = getMediaFile(ref);
|
|
52
|
+
if (!row) return false;
|
|
53
|
+
const file = new expo_file_system.File(row.uri);
|
|
54
|
+
if (file.exists) await file.delete();
|
|
55
|
+
deleteMediaFileRow(ref);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
function mediaDirectory(kind) {
|
|
59
|
+
return new expo_file_system.Directory(expo_file_system.Paths.document, "silo", "media", require_shared.kindDirectoryName(kind));
|
|
60
|
+
}
|
|
61
|
+
function getMediaFile(ref) {
|
|
62
|
+
return getDatabase().getFirstSync(`SELECT ref, uri FROM media_files WHERE ref = ?`, [ref]);
|
|
63
|
+
}
|
|
64
|
+
function getDatabase() {
|
|
65
|
+
if (!database) {
|
|
66
|
+
database = (0, expo_sqlite.openDatabaseSync)("silo.db");
|
|
67
|
+
database.execSync(`
|
|
68
|
+
PRAGMA journal_mode = WAL;
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS media_files (
|
|
71
|
+
ref TEXT PRIMARY KEY,
|
|
72
|
+
uri TEXT NOT NULL,
|
|
73
|
+
kind TEXT NOT NULL,
|
|
74
|
+
name TEXT NOT NULL,
|
|
75
|
+
mimeType TEXT,
|
|
76
|
+
size INTEGER NOT NULL,
|
|
77
|
+
createdAt TEXT NOT NULL,
|
|
78
|
+
data TEXT NOT NULL
|
|
79
|
+
);
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
return database;
|
|
83
|
+
}
|
|
84
|
+
function insertMediaFile(file) {
|
|
85
|
+
getDatabase().runSync(`INSERT INTO media_files (ref, uri, kind, name, mimeType, size, createdAt, data)
|
|
86
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
87
|
+
file.ref,
|
|
88
|
+
file.uri,
|
|
89
|
+
file.kind,
|
|
90
|
+
file.name,
|
|
91
|
+
file.mimeType,
|
|
92
|
+
file.size,
|
|
93
|
+
file.createdAt,
|
|
94
|
+
JSON.stringify(file.data)
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
function deleteMediaFileRow(ref) {
|
|
98
|
+
getDatabase().runSync(`DELETE FROM media_files WHERE ref = ?`, [ref]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
exports.mediaStorage = mediaStorage;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createMediaRef, extensionFor, kindDirectoryName } from "./shared.mjs";
|
|
2
|
+
import { randomUUID } from "expo-crypto";
|
|
3
|
+
import { openDatabaseSync } from "expo-sqlite";
|
|
4
|
+
import { Directory, File, Paths } from "expo-file-system";
|
|
5
|
+
|
|
6
|
+
//#region src/media/storage.native.ts
|
|
7
|
+
let database;
|
|
8
|
+
const mediaStorage = {
|
|
9
|
+
importMedia,
|
|
10
|
+
resolveMediaUri,
|
|
11
|
+
deleteMedia
|
|
12
|
+
};
|
|
13
|
+
async function importMedia(input) {
|
|
14
|
+
const id = randomUUID();
|
|
15
|
+
const ref = createMediaRef(input.kind, id);
|
|
16
|
+
const source = new File(input.uri);
|
|
17
|
+
const name = input.name ?? source.name ?? `${id}${source.extension}`;
|
|
18
|
+
const destinationDirectory = mediaDirectory(input.kind);
|
|
19
|
+
destinationDirectory.create({
|
|
20
|
+
idempotent: true,
|
|
21
|
+
intermediates: true
|
|
22
|
+
});
|
|
23
|
+
const destination = new File(destinationDirectory, `${id}${extensionFor(name)}`);
|
|
24
|
+
await source.copy(destination);
|
|
25
|
+
const info = destination.info();
|
|
26
|
+
const mimeType = input.mimeType ?? (destination.type || null);
|
|
27
|
+
const size = info.size ?? destination.size;
|
|
28
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
29
|
+
insertMediaFile({
|
|
30
|
+
ref,
|
|
31
|
+
uri: destination.uri,
|
|
32
|
+
kind: input.kind,
|
|
33
|
+
name,
|
|
34
|
+
mimeType,
|
|
35
|
+
size,
|
|
36
|
+
createdAt: timestamp,
|
|
37
|
+
data: input.data ?? {}
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
ref,
|
|
41
|
+
uri: destination.uri
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function resolveMediaUri(ref) {
|
|
45
|
+
const row = getMediaFile(ref);
|
|
46
|
+
if (!row) return null;
|
|
47
|
+
return new File(row.uri).exists ? row.uri : null;
|
|
48
|
+
}
|
|
49
|
+
async function deleteMedia(ref) {
|
|
50
|
+
const row = getMediaFile(ref);
|
|
51
|
+
if (!row) return false;
|
|
52
|
+
const file = new File(row.uri);
|
|
53
|
+
if (file.exists) await file.delete();
|
|
54
|
+
deleteMediaFileRow(ref);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
function mediaDirectory(kind) {
|
|
58
|
+
return new Directory(Paths.document, "silo", "media", kindDirectoryName(kind));
|
|
59
|
+
}
|
|
60
|
+
function getMediaFile(ref) {
|
|
61
|
+
return getDatabase().getFirstSync(`SELECT ref, uri FROM media_files WHERE ref = ?`, [ref]);
|
|
62
|
+
}
|
|
63
|
+
function getDatabase() {
|
|
64
|
+
if (!database) {
|
|
65
|
+
database = openDatabaseSync("silo.db");
|
|
66
|
+
database.execSync(`
|
|
67
|
+
PRAGMA journal_mode = WAL;
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS media_files (
|
|
70
|
+
ref TEXT PRIMARY KEY,
|
|
71
|
+
uri TEXT NOT NULL,
|
|
72
|
+
kind TEXT NOT NULL,
|
|
73
|
+
name TEXT NOT NULL,
|
|
74
|
+
mimeType TEXT,
|
|
75
|
+
size INTEGER NOT NULL,
|
|
76
|
+
createdAt TEXT NOT NULL,
|
|
77
|
+
data TEXT NOT NULL
|
|
78
|
+
);
|
|
79
|
+
`);
|
|
80
|
+
}
|
|
81
|
+
return database;
|
|
82
|
+
}
|
|
83
|
+
function insertMediaFile(file) {
|
|
84
|
+
getDatabase().runSync(`INSERT INTO media_files (ref, uri, kind, name, mimeType, size, createdAt, data)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
86
|
+
file.ref,
|
|
87
|
+
file.uri,
|
|
88
|
+
file.kind,
|
|
89
|
+
file.name,
|
|
90
|
+
file.mimeType,
|
|
91
|
+
file.size,
|
|
92
|
+
file.createdAt,
|
|
93
|
+
JSON.stringify(file.data)
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
function deleteMediaFileRow(ref) {
|
|
97
|
+
getDatabase().runSync(`DELETE FROM media_files WHERE ref = ?`, [ref]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
export { mediaStorage };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_indexed_db = require('../internal/indexed-db.cjs');
|
|
3
|
+
const require_shared = require('./shared.cjs');
|
|
4
|
+
let expo_crypto = require("expo-crypto");
|
|
5
|
+
|
|
6
|
+
//#region src/media/storage.web.ts
|
|
7
|
+
const objectUrls = /* @__PURE__ */ new Map();
|
|
8
|
+
const mediaStorage = {
|
|
9
|
+
importMedia,
|
|
10
|
+
resolveMediaUri,
|
|
11
|
+
deleteMedia
|
|
12
|
+
};
|
|
13
|
+
const openSiloMediaIndexedDb = require_indexed_db.createIndexedDbOpener("silo-media", 1, (db) => {
|
|
14
|
+
if (!db.objectStoreNames.contains("media_files")) db.createObjectStore("media_files", { keyPath: "ref" });
|
|
15
|
+
if (!db.objectStoreNames.contains("media_blobs")) db.createObjectStore("media_blobs", { keyPath: "ref" });
|
|
16
|
+
});
|
|
17
|
+
async function importMedia(input) {
|
|
18
|
+
const id = (0, expo_crypto.randomUUID)();
|
|
19
|
+
const ref = require_shared.createMediaRef(input.kind, id);
|
|
20
|
+
const blob = await (await fetch(input.uri)).blob();
|
|
21
|
+
const name = input.name ?? `${id}${require_shared.extensionForMimeType(input.mimeType ?? blob.type)}`;
|
|
22
|
+
const mimeType = input.mimeType ?? (blob.type || null);
|
|
23
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
24
|
+
const transaction = (await openSiloMediaIndexedDb()).transaction(["media_files", "media_blobs"], "readwrite");
|
|
25
|
+
transaction.objectStore("media_files").put({
|
|
26
|
+
ref,
|
|
27
|
+
uri: ref,
|
|
28
|
+
kind: input.kind,
|
|
29
|
+
name,
|
|
30
|
+
mimeType,
|
|
31
|
+
size: blob.size,
|
|
32
|
+
createdAt: timestamp,
|
|
33
|
+
data: JSON.stringify(input.data ?? {})
|
|
34
|
+
});
|
|
35
|
+
transaction.objectStore("media_blobs").put({
|
|
36
|
+
ref,
|
|
37
|
+
blob
|
|
38
|
+
});
|
|
39
|
+
await require_indexed_db.transactionDone(transaction);
|
|
40
|
+
const uri = URL.createObjectURL(blob);
|
|
41
|
+
objectUrls.set(ref, uri);
|
|
42
|
+
return {
|
|
43
|
+
ref,
|
|
44
|
+
uri
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function resolveMediaUri(ref) {
|
|
48
|
+
const existingUrl = objectUrls.get(ref);
|
|
49
|
+
if (existingUrl) return existingUrl;
|
|
50
|
+
const transaction = (await openSiloMediaIndexedDb()).transaction("media_blobs", "readonly");
|
|
51
|
+
const done = require_indexed_db.transactionDone(transaction);
|
|
52
|
+
const row = await require_indexed_db.requestResult(transaction.objectStore("media_blobs").get(ref));
|
|
53
|
+
await done;
|
|
54
|
+
if (!row) return null;
|
|
55
|
+
const uri = URL.createObjectURL(row.blob);
|
|
56
|
+
objectUrls.set(ref, uri);
|
|
57
|
+
return uri;
|
|
58
|
+
}
|
|
59
|
+
async function deleteMedia(ref) {
|
|
60
|
+
const db = await openSiloMediaIndexedDb();
|
|
61
|
+
const readTransaction = db.transaction("media_files", "readonly");
|
|
62
|
+
const readDone = require_indexed_db.transactionDone(readTransaction);
|
|
63
|
+
const existing = await require_indexed_db.requestResult(readTransaction.objectStore("media_files").get(ref));
|
|
64
|
+
await readDone;
|
|
65
|
+
if (!existing) return false;
|
|
66
|
+
const transaction = db.transaction(["media_files", "media_blobs"], "readwrite");
|
|
67
|
+
transaction.objectStore("media_files").delete(ref);
|
|
68
|
+
transaction.objectStore("media_blobs").delete(ref);
|
|
69
|
+
await require_indexed_db.transactionDone(transaction);
|
|
70
|
+
const objectUrl = objectUrls.get(ref);
|
|
71
|
+
if (objectUrl) {
|
|
72
|
+
URL.revokeObjectURL(objectUrl);
|
|
73
|
+
objectUrls.delete(ref);
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
exports.mediaStorage = mediaStorage;
|