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.
Files changed (52) hide show
  1. package/README.md +29 -5
  2. package/dist/internal/indexed-db.cjs +44 -0
  3. package/dist/internal/indexed-db.mjs +41 -0
  4. package/dist/media/shared.cjs +99 -0
  5. package/dist/media/shared.d.cts +26 -0
  6. package/dist/media/shared.d.mts +26 -0
  7. package/dist/media/shared.mjs +93 -0
  8. package/dist/media/storage.native.cjs +102 -0
  9. package/dist/media/storage.native.mjs +101 -0
  10. package/dist/media/storage.web.cjs +79 -0
  11. package/dist/media/storage.web.mjs +78 -0
  12. package/dist/media.cjs +5 -131
  13. package/dist/media.d.cts +8 -25
  14. package/dist/media.d.mts +8 -25
  15. package/dist/media.mjs +4 -129
  16. package/dist/media.web.cjs +9 -0
  17. package/dist/media.web.d.cts +12 -0
  18. package/dist/media.web.d.mts +12 -0
  19. package/dist/media.web.mjs +8 -0
  20. package/dist/settings/shared.cjs +63 -0
  21. package/dist/settings/shared.d.cts +19 -0
  22. package/dist/settings/shared.d.mts +19 -0
  23. package/dist/settings/shared.mjs +59 -0
  24. package/dist/settings/storage.native.cjs +24 -0
  25. package/dist/settings/storage.native.mjs +22 -0
  26. package/dist/settings/storage.web.cjs +34 -0
  27. package/dist/settings/storage.web.mjs +34 -0
  28. package/dist/settings.cjs +4 -66
  29. package/dist/settings.d.cts +4 -18
  30. package/dist/settings.d.mts +4 -18
  31. package/dist/settings.mjs +4 -64
  32. package/dist/settings.web.cjs +8 -0
  33. package/dist/settings.web.d.cts +6 -0
  34. package/dist/settings.web.d.mts +6 -0
  35. package/dist/settings.web.mjs +8 -0
  36. package/dist/store/shared.cjs +338 -0
  37. package/dist/store/shared.d.cts +58 -0
  38. package/dist/store/shared.d.mts +58 -0
  39. package/dist/store/shared.mjs +333 -0
  40. package/dist/store/storage.native.cjs +133 -0
  41. package/dist/store/storage.native.mjs +132 -0
  42. package/dist/store/storage.web.cjs +142 -0
  43. package/dist/store/storage.web.mjs +142 -0
  44. package/dist/store.cjs +4 -421
  45. package/dist/store.d.cts +4 -58
  46. package/dist/store.d.mts +4 -58
  47. package/dist/store.mjs +4 -421
  48. package/dist/store.web.cjs +15 -0
  49. package/dist/store.web.d.cts +7 -0
  50. package/dist/store.web.d.mts +7 -0
  51. package/dist/store.web.mjs +12 -0
  52. package/package.json +31 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@ TanStack hooks.
6
6
 
7
7
  Silo owns:
8
8
 
9
- - SQLite persistence
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 SQLite-backed document collections and a link collection for
31
- relationships between them. They produce options for TanStack DB collections.
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 backed by `expo-sqlite/kv-store`.
158
- They produce TanStack Query options.
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;