silosdk 0.0.2 → 0.0.4

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 ADDED
@@ -0,0 +1,162 @@
1
+ # Silo SDK
2
+
3
+ Silo SDK provides local-first persistence primitives for Expo apps. It owns local
4
+ storage and returns TanStack-compatible option objects instead of wrapping
5
+ TanStack hooks.
6
+
7
+ Silo owns:
8
+
9
+ - SQLite persistence
10
+ - flat primitive document constraints
11
+ - defaults-first source shape
12
+ - settings storage
13
+ - media file ownership and resolution
14
+
15
+ TanStack owns:
16
+
17
+ - `createCollection()` and live collection queries
18
+ - `useQuery()` and `useMutation()`
19
+ - query and mutation state
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ yarn add silosdk @tanstack/react-db @tanstack/react-query expo-sqlite expo-file-system
25
+ ```
26
+
27
+ ## Sources
28
+
29
+ Sources are SQLite-backed document stores that produce options for TanStack DB
30
+ collections.
31
+
32
+ ```ts
33
+ import { createCollection } from '@tanstack/react-db'
34
+ import { source } from 'silosdk/source'
35
+
36
+ type Post = {
37
+ title: string
38
+ body: string
39
+ published: boolean
40
+ coverImage: string | null
41
+ }
42
+
43
+ export const postsSource = source<Post>('posts', {
44
+ title: '',
45
+ body: '',
46
+ published: false,
47
+ coverImage: null,
48
+ })
49
+
50
+ export const posts = createCollection(postsSource.collectionOptions())
51
+ ```
52
+
53
+ Query and mutate with TanStack DB directly:
54
+
55
+ ```tsx
56
+ import { eq, useLiveQuery } from '@tanstack/react-db'
57
+
58
+ const { data = [] } = useLiveQuery((q) =>
59
+ q.from({ posts }).where(({ posts }) => eq(posts.published, true)),
60
+ )
61
+
62
+ const tx = posts.insert({
63
+ id: crypto.randomUUID(),
64
+ title: 'Hello',
65
+ body: '',
66
+ published: false,
67
+ coverImage: null,
68
+ })
69
+
70
+ await tx.isPersisted.promise
71
+ ```
72
+
73
+ Source rows are intentionally flat. Field values must be:
74
+
75
+ ```ts
76
+ type FieldValue = string | number | boolean | null
77
+ ```
78
+
79
+ ## Settings
80
+
81
+ Settings are small persisted primitive values backed by `expo-sqlite/kv-store`.
82
+ They produce TanStack Query options.
83
+
84
+ ```ts
85
+ import { setting } from 'silosdk/settings'
86
+
87
+ export const theme = setting<'system' | 'light' | 'dark'>('theme', 'system')
88
+ export const reduceMotion = setting('reduceMotion', false)
89
+ ```
90
+
91
+ ```tsx
92
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
93
+
94
+ const queryClient = useQueryClient()
95
+ const { data: value } = useQuery(theme.queryOptions())
96
+ const setTheme = useMutation(theme.mutationOptions({ queryClient }))
97
+
98
+ setTheme.mutate('dark')
99
+ setTheme.mutate((current) => (current === 'dark' ? 'light' : 'dark'))
100
+ ```
101
+
102
+ Setting values must also be flat primitives:
103
+
104
+ ```ts
105
+ type SettingValue = string | number | boolean | null
106
+ ```
107
+
108
+ ## Media
109
+
110
+ Media is singleton infrastructure for app-owned files. Domain documents store
111
+ stable media refs as plain strings, keeping source rows flat.
112
+
113
+ ```ts
114
+ import { media, type MediaRef } from 'silosdk/media'
115
+ ```
116
+
117
+ Import a file with TanStack Query mutation options:
118
+
119
+ ```tsx
120
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
121
+
122
+ const queryClient = useQueryClient()
123
+ const importMedia = useMutation(media.mutationOptions({ queryClient }))
124
+
125
+ const coverImage = await importMedia.mutateAsync({
126
+ uri: pickedFile.uri,
127
+ kind: 'image',
128
+ name: pickedFile.name,
129
+ mimeType: pickedFile.type,
130
+ })
131
+ ```
132
+
133
+ Store the returned `media://...` ref in source rows:
134
+
135
+ ```ts
136
+ posts.update(postId, (draft) => {
137
+ draft.coverImage = coverImage
138
+ })
139
+ ```
140
+
141
+ Resolve a media ref to a local file URI:
142
+
143
+ ```tsx
144
+ const { data: uri } = useQuery(media.queryOptions(post.coverImage))
145
+ ```
146
+
147
+ `media.queryOptions()` returns `string | null`. It returns `null` when the ref is
148
+ empty, invalid, unknown, or missing on disk.
149
+
150
+ ## API Shape
151
+
152
+ Silo exposes definition objects and option factories:
153
+
154
+ ```ts
155
+ createCollection(postsSource.collectionOptions())
156
+ useQuery(theme.queryOptions())
157
+ useMutation(theme.mutationOptions({ queryClient }))
158
+ useQuery(media.queryOptions(ref))
159
+ useMutation(media.mutationOptions({ queryClient }))
160
+ ```
161
+
162
+ There is no Silo provider and no Silo hook wrapper API.
@@ -0,0 +1,29 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+
29
+ exports.__toESM = __toESM;
package/dist/media.cjs ADDED
@@ -0,0 +1,135 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ let expo_sqlite = require("expo-sqlite");
3
+ let __tanstack_react_query = require("@tanstack/react-query");
4
+ let expo_file_system = require("expo-file-system");
5
+ let nanoid = require("nanoid");
6
+
7
+ //#region src/media/index.ts
8
+ const media = {
9
+ queryOptions(ref) {
10
+ return (0, __tanstack_react_query.queryOptions)({
11
+ queryKey: mediaQueryKey(ref),
12
+ queryFn: () => resolveMediaUri(ref)
13
+ });
14
+ },
15
+ mutationOptions(options = {}) {
16
+ return (0, __tanstack_react_query.mutationOptions)({
17
+ mutationKey: [
18
+ "silo",
19
+ "media",
20
+ "import"
21
+ ],
22
+ mutationFn: async (input) => {
23
+ const imported = await importMedia(input);
24
+ options.queryClient?.setQueryData(mediaQueryKey(imported.ref), imported.uri);
25
+ return imported.ref;
26
+ }
27
+ });
28
+ }
29
+ };
30
+ function isMediaRef(value) {
31
+ return typeof value === "string" && value.startsWith("media://");
32
+ }
33
+ function mediaQueryKey(ref) {
34
+ return [
35
+ "silo",
36
+ "media",
37
+ ref ?? null
38
+ ];
39
+ }
40
+ async function importMedia(input) {
41
+ assertMediaKind(input.kind);
42
+ const id = (0, nanoid.nanoid)();
43
+ const ref = createMediaRef(input.kind, id);
44
+ const source = new expo_file_system.File(input.uri);
45
+ const name = input.name ?? source.name ?? `${id}${source.extension}`;
46
+ const destinationDirectory = mediaDirectory(input.kind);
47
+ destinationDirectory.create({
48
+ idempotent: true,
49
+ intermediates: true
50
+ });
51
+ const destination = new expo_file_system.File(destinationDirectory, `${id}${extensionFor(name)}`);
52
+ await source.copy(destination);
53
+ const info = destination.info();
54
+ const mimeType = input.mimeType ?? (destination.type || null);
55
+ const size = info.size ?? destination.size;
56
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
57
+ insertMediaFile({
58
+ ref,
59
+ uri: destination.uri,
60
+ kind: input.kind,
61
+ name,
62
+ mimeType,
63
+ size,
64
+ createdAt: timestamp,
65
+ data: input.data ?? {}
66
+ });
67
+ return {
68
+ ref,
69
+ uri: destination.uri
70
+ };
71
+ }
72
+ function resolveMediaUri(ref) {
73
+ if (!isMediaRef(ref)) return null;
74
+ const row = getDatabase().getFirstSync(`SELECT ref, uri FROM media_files WHERE ref = ?`, [ref]);
75
+ if (!row) return null;
76
+ return new expo_file_system.File(row.uri).exists ? row.uri : null;
77
+ }
78
+ function createMediaRef(kind, id) {
79
+ return `media://silo/${kindDirectoryName(kind)}/${id}`;
80
+ }
81
+ function mediaDirectory(kind) {
82
+ return new expo_file_system.Directory(expo_file_system.Paths.document, "silo", "media", kindDirectoryName(kind));
83
+ }
84
+ function kindDirectoryName(kind) {
85
+ switch (kind) {
86
+ case "image": return "images";
87
+ case "video": return "videos";
88
+ case "audio": return "audio";
89
+ case "file": return "files";
90
+ }
91
+ }
92
+ function extensionFor(name) {
93
+ return name.match(/\.[A-Za-z0-9]+$/)?.[0] ?? "";
94
+ }
95
+ function assertMediaKind(kind) {
96
+ if (kind !== "image" && kind !== "video" && kind !== "audio" && kind !== "file") throw new Error("Media kind must be one of \"image\", \"video\", \"audio\", or \"file\".");
97
+ }
98
+ let database;
99
+ function getDatabase() {
100
+ if (!database) {
101
+ database = (0, expo_sqlite.openDatabaseSync)("silo.db");
102
+ database.execSync(`
103
+ PRAGMA journal_mode = WAL;
104
+
105
+ CREATE TABLE IF NOT EXISTS media_files (
106
+ ref TEXT PRIMARY KEY,
107
+ uri TEXT NOT NULL,
108
+ kind TEXT NOT NULL,
109
+ name TEXT NOT NULL,
110
+ mimeType TEXT,
111
+ size INTEGER NOT NULL,
112
+ createdAt TEXT NOT NULL,
113
+ data TEXT NOT NULL
114
+ );
115
+ `);
116
+ }
117
+ return database;
118
+ }
119
+ function insertMediaFile(file) {
120
+ getDatabase().runSync(`INSERT INTO media_files (ref, uri, kind, name, mimeType, size, createdAt, data)
121
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
122
+ file.ref,
123
+ file.uri,
124
+ file.kind,
125
+ file.name,
126
+ file.mimeType,
127
+ file.size,
128
+ file.createdAt,
129
+ JSON.stringify(file.data)
130
+ ]);
131
+ }
132
+
133
+ //#endregion
134
+ exports.isMediaRef = isMediaRef;
135
+ exports.media = media;
@@ -0,0 +1,29 @@
1
+ import { QueryClient, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
2
+
3
+ //#region src/media/index.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 MediaQueryKey = readonly ['silo', 'media', MediaRef | string | null];
20
+ type MediaMutationOptions = {
21
+ queryClient?: QueryClient;
22
+ };
23
+ declare const media: {
24
+ queryOptions(ref: MediaRef | string | null | undefined): UseQueryOptions<string | null, Error, string | null, MediaQueryKey>;
25
+ mutationOptions(options?: MediaMutationOptions): UseMutationOptions<MediaRef, Error, MediaImportInput>;
26
+ };
27
+ declare function isMediaRef(value: unknown): value is MediaRef;
28
+ //#endregion
29
+ export { MediaData, MediaImportInput, MediaJson, MediaKind, MediaMutationOptions, MediaQueryKey, MediaRef, isMediaRef, media };
@@ -0,0 +1,29 @@
1
+ import { QueryClient, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
2
+
3
+ //#region src/media/index.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 MediaQueryKey = readonly ['silo', 'media', MediaRef | string | null];
20
+ type MediaMutationOptions = {
21
+ queryClient?: QueryClient;
22
+ };
23
+ declare const media: {
24
+ queryOptions(ref: MediaRef | string | null | undefined): UseQueryOptions<string | null, Error, string | null, MediaQueryKey>;
25
+ mutationOptions(options?: MediaMutationOptions): UseMutationOptions<MediaRef, Error, MediaImportInput>;
26
+ };
27
+ declare function isMediaRef(value: unknown): value is MediaRef;
28
+ //#endregion
29
+ export { MediaData, MediaImportInput, MediaJson, MediaKind, MediaMutationOptions, MediaQueryKey, MediaRef, isMediaRef, media };
package/dist/media.mjs ADDED
@@ -0,0 +1,133 @@
1
+ import { openDatabaseSync } from "expo-sqlite";
2
+ import { mutationOptions, queryOptions } from "@tanstack/react-query";
3
+ import { Directory, File, Paths } from "expo-file-system";
4
+ import { nanoid } from "nanoid";
5
+
6
+ //#region src/media/index.ts
7
+ const media = {
8
+ queryOptions(ref) {
9
+ return queryOptions({
10
+ queryKey: mediaQueryKey(ref),
11
+ queryFn: () => resolveMediaUri(ref)
12
+ });
13
+ },
14
+ mutationOptions(options = {}) {
15
+ return mutationOptions({
16
+ mutationKey: [
17
+ "silo",
18
+ "media",
19
+ "import"
20
+ ],
21
+ mutationFn: async (input) => {
22
+ const imported = await importMedia(input);
23
+ options.queryClient?.setQueryData(mediaQueryKey(imported.ref), imported.uri);
24
+ return imported.ref;
25
+ }
26
+ });
27
+ }
28
+ };
29
+ function isMediaRef(value) {
30
+ return typeof value === "string" && value.startsWith("media://");
31
+ }
32
+ function mediaQueryKey(ref) {
33
+ return [
34
+ "silo",
35
+ "media",
36
+ ref ?? null
37
+ ];
38
+ }
39
+ async function importMedia(input) {
40
+ assertMediaKind(input.kind);
41
+ const id = nanoid();
42
+ const ref = createMediaRef(input.kind, id);
43
+ const source = new File(input.uri);
44
+ const name = input.name ?? source.name ?? `${id}${source.extension}`;
45
+ const destinationDirectory = mediaDirectory(input.kind);
46
+ destinationDirectory.create({
47
+ idempotent: true,
48
+ intermediates: true
49
+ });
50
+ const destination = new File(destinationDirectory, `${id}${extensionFor(name)}`);
51
+ await source.copy(destination);
52
+ const info = destination.info();
53
+ const mimeType = input.mimeType ?? (destination.type || null);
54
+ const size = info.size ?? destination.size;
55
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
56
+ insertMediaFile({
57
+ ref,
58
+ uri: destination.uri,
59
+ kind: input.kind,
60
+ name,
61
+ mimeType,
62
+ size,
63
+ createdAt: timestamp,
64
+ data: input.data ?? {}
65
+ });
66
+ return {
67
+ ref,
68
+ uri: destination.uri
69
+ };
70
+ }
71
+ function resolveMediaUri(ref) {
72
+ if (!isMediaRef(ref)) return null;
73
+ const row = getDatabase().getFirstSync(`SELECT ref, uri FROM media_files WHERE ref = ?`, [ref]);
74
+ if (!row) return null;
75
+ return new File(row.uri).exists ? row.uri : null;
76
+ }
77
+ function createMediaRef(kind, id) {
78
+ return `media://silo/${kindDirectoryName(kind)}/${id}`;
79
+ }
80
+ function mediaDirectory(kind) {
81
+ return new Directory(Paths.document, "silo", "media", kindDirectoryName(kind));
82
+ }
83
+ function kindDirectoryName(kind) {
84
+ switch (kind) {
85
+ case "image": return "images";
86
+ case "video": return "videos";
87
+ case "audio": return "audio";
88
+ case "file": return "files";
89
+ }
90
+ }
91
+ function extensionFor(name) {
92
+ return name.match(/\.[A-Za-z0-9]+$/)?.[0] ?? "";
93
+ }
94
+ function assertMediaKind(kind) {
95
+ if (kind !== "image" && kind !== "video" && kind !== "audio" && kind !== "file") throw new Error("Media kind must be one of \"image\", \"video\", \"audio\", or \"file\".");
96
+ }
97
+ let database;
98
+ function getDatabase() {
99
+ if (!database) {
100
+ database = openDatabaseSync("silo.db");
101
+ database.execSync(`
102
+ PRAGMA journal_mode = WAL;
103
+
104
+ CREATE TABLE IF NOT EXISTS media_files (
105
+ ref TEXT PRIMARY KEY,
106
+ uri TEXT NOT NULL,
107
+ kind TEXT NOT NULL,
108
+ name TEXT NOT NULL,
109
+ mimeType TEXT,
110
+ size INTEGER NOT NULL,
111
+ createdAt TEXT NOT NULL,
112
+ data TEXT NOT NULL
113
+ );
114
+ `);
115
+ }
116
+ return database;
117
+ }
118
+ function insertMediaFile(file) {
119
+ getDatabase().runSync(`INSERT INTO media_files (ref, uri, kind, name, mimeType, size, createdAt, data)
120
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
121
+ file.ref,
122
+ file.uri,
123
+ file.kind,
124
+ file.name,
125
+ file.mimeType,
126
+ file.size,
127
+ file.createdAt,
128
+ JSON.stringify(file.data)
129
+ ]);
130
+ }
131
+
132
+ //#endregion
133
+ export { isMediaRef, media };
@@ -0,0 +1,70 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ let __tanstack_react_query = require("@tanstack/react-query");
3
+ let expo_sqlite_kv_store = require("expo-sqlite/kv-store");
4
+ expo_sqlite_kv_store = require_rolldown_runtime.__toESM(expo_sqlite_kv_store);
5
+
6
+ //#region src/settings/index.ts
7
+ const settingNames = /* @__PURE__ */ new Set();
8
+ const settingNamePattern = /^[A-Za-z][A-Za-z0-9_.:-]*$/;
9
+ function setting(name, defaultValue) {
10
+ assertSettingName(name);
11
+ assertSettingValue(resolveDefault(defaultValue));
12
+ if (settingNames.has(name)) throw new Error(`Setting "${name}" is already registered. Define each setting once and import the existing setting instead.`);
13
+ settingNames.add(name);
14
+ const queryKey = [
15
+ "silo",
16
+ "setting",
17
+ name
18
+ ];
19
+ return {
20
+ name,
21
+ defaultValue,
22
+ queryKey,
23
+ queryOptions() {
24
+ return (0, __tanstack_react_query.queryOptions)({
25
+ queryKey,
26
+ queryFn: () => readSetting(name, defaultValue)
27
+ });
28
+ },
29
+ mutationOptions(options = {}) {
30
+ return (0, __tanstack_react_query.mutationOptions)({
31
+ mutationKey: queryKey,
32
+ mutationFn: async (valueOrUpdater) => {
33
+ const current = readSetting(name, defaultValue);
34
+ const next = typeof valueOrUpdater === "function" ? valueOrUpdater(current) : valueOrUpdater;
35
+ writeSetting(name, next);
36
+ return next;
37
+ },
38
+ onSuccess(value) {
39
+ options.queryClient?.setQueryData(queryKey, value);
40
+ }
41
+ });
42
+ }
43
+ };
44
+ }
45
+ function readSetting(name, defaultValue) {
46
+ const stored = expo_sqlite_kv_store.default.getItemSync(storageKey(name));
47
+ if (stored === null) return resolveDefault(defaultValue);
48
+ const value = JSON.parse(stored);
49
+ assertSettingValue(value);
50
+ return value;
51
+ }
52
+ function writeSetting(name, value) {
53
+ assertSettingValue(value);
54
+ expo_sqlite_kv_store.default.setItemSync(storageKey(name), JSON.stringify(value));
55
+ }
56
+ function storageKey(name) {
57
+ return `silo:setting:${name}`;
58
+ }
59
+ function resolveDefault(defaultValue) {
60
+ return typeof defaultValue === "function" ? defaultValue() : defaultValue;
61
+ }
62
+ function assertSettingName(name) {
63
+ if (!settingNamePattern.test(name)) throw new Error(`Setting names must start with a letter and contain only letters, numbers, underscores, dots, colons, or hyphens. Received "${name}".`);
64
+ }
65
+ function assertSettingValue(value) {
66
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" && value !== null) throw new Error("Settings must be strings, numbers, booleans, or null.");
67
+ }
68
+
69
+ //#endregion
70
+ exports.setting = setting;
@@ -0,0 +1,20 @@
1
+ import { QueryClient, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
2
+
3
+ //#region src/settings/index.d.ts
4
+ type SettingValue = string | number | boolean | null;
5
+ type SettingDefault<T extends SettingValue> = T | (() => T);
6
+ type SettingMutation<T extends SettingValue> = T | ((current: T) => T);
7
+ type SettingQueryKey = readonly ['silo', 'setting', string];
8
+ type SettingMutationOptions = {
9
+ queryClient?: QueryClient;
10
+ };
11
+ type Setting<T extends SettingValue> = {
12
+ readonly name: string;
13
+ readonly defaultValue: SettingDefault<T>;
14
+ readonly queryKey: SettingQueryKey;
15
+ queryOptions(): UseQueryOptions<T, Error, T, SettingQueryKey>;
16
+ mutationOptions(options?: SettingMutationOptions): UseMutationOptions<T, Error, SettingMutation<T>>;
17
+ };
18
+ declare function setting<T extends SettingValue>(name: string, defaultValue: SettingDefault<T>): Setting<T>;
19
+ //#endregion
20
+ export { Setting, SettingDefault, SettingMutation, SettingMutationOptions, SettingQueryKey, SettingValue, setting };
@@ -0,0 +1,20 @@
1
+ import { QueryClient, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
2
+
3
+ //#region src/settings/index.d.ts
4
+ type SettingValue = string | number | boolean | null;
5
+ type SettingDefault<T extends SettingValue> = T | (() => T);
6
+ type SettingMutation<T extends SettingValue> = T | ((current: T) => T);
7
+ type SettingQueryKey = readonly ['silo', 'setting', string];
8
+ type SettingMutationOptions = {
9
+ queryClient?: QueryClient;
10
+ };
11
+ type Setting<T extends SettingValue> = {
12
+ readonly name: string;
13
+ readonly defaultValue: SettingDefault<T>;
14
+ readonly queryKey: SettingQueryKey;
15
+ queryOptions(): UseQueryOptions<T, Error, T, SettingQueryKey>;
16
+ mutationOptions(options?: SettingMutationOptions): UseMutationOptions<T, Error, SettingMutation<T>>;
17
+ };
18
+ declare function setting<T extends SettingValue>(name: string, defaultValue: SettingDefault<T>): Setting<T>;
19
+ //#endregion
20
+ export { Setting, SettingDefault, SettingMutation, SettingMutationOptions, SettingQueryKey, SettingValue, setting };
@@ -0,0 +1,68 @@
1
+ import { mutationOptions, queryOptions } from "@tanstack/react-query";
2
+ import Storage from "expo-sqlite/kv-store";
3
+
4
+ //#region src/settings/index.ts
5
+ const settingNames = /* @__PURE__ */ new Set();
6
+ const settingNamePattern = /^[A-Za-z][A-Za-z0-9_.:-]*$/;
7
+ function setting(name, defaultValue) {
8
+ assertSettingName(name);
9
+ assertSettingValue(resolveDefault(defaultValue));
10
+ if (settingNames.has(name)) throw new Error(`Setting "${name}" is already registered. Define each setting once and import the existing setting instead.`);
11
+ settingNames.add(name);
12
+ const queryKey = [
13
+ "silo",
14
+ "setting",
15
+ name
16
+ ];
17
+ return {
18
+ name,
19
+ defaultValue,
20
+ queryKey,
21
+ queryOptions() {
22
+ return queryOptions({
23
+ queryKey,
24
+ queryFn: () => readSetting(name, defaultValue)
25
+ });
26
+ },
27
+ mutationOptions(options = {}) {
28
+ return mutationOptions({
29
+ mutationKey: queryKey,
30
+ mutationFn: async (valueOrUpdater) => {
31
+ const current = readSetting(name, defaultValue);
32
+ const next = typeof valueOrUpdater === "function" ? valueOrUpdater(current) : valueOrUpdater;
33
+ writeSetting(name, next);
34
+ return next;
35
+ },
36
+ onSuccess(value) {
37
+ options.queryClient?.setQueryData(queryKey, value);
38
+ }
39
+ });
40
+ }
41
+ };
42
+ }
43
+ function readSetting(name, defaultValue) {
44
+ const stored = Storage.getItemSync(storageKey(name));
45
+ if (stored === null) return resolveDefault(defaultValue);
46
+ const value = JSON.parse(stored);
47
+ assertSettingValue(value);
48
+ return value;
49
+ }
50
+ function writeSetting(name, value) {
51
+ assertSettingValue(value);
52
+ Storage.setItemSync(storageKey(name), JSON.stringify(value));
53
+ }
54
+ function storageKey(name) {
55
+ return `silo:setting:${name}`;
56
+ }
57
+ function resolveDefault(defaultValue) {
58
+ return typeof defaultValue === "function" ? defaultValue() : defaultValue;
59
+ }
60
+ function assertSettingName(name) {
61
+ if (!settingNamePattern.test(name)) throw new Error(`Setting names must start with a letter and contain only letters, numbers, underscores, dots, colons, or hyphens. Received "${name}".`);
62
+ }
63
+ function assertSettingValue(value) {
64
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" && value !== null) throw new Error("Settings must be strings, numbers, booleans, or null.");
65
+ }
66
+
67
+ //#endregion
68
+ export { setting };
package/dist/source.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
1
2
  let expo_sqlite = require("expo-sqlite");
2
3
 
3
4
  //#region src/source/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silosdk",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsdown",
@@ -17,6 +17,7 @@
17
17
  "@tanstack/react-query": "^5.101.0",
18
18
  "@types/node": "^25.3.0",
19
19
  "@types/react": "^19",
20
+ "expo-file-system": "^56.0.8",
20
21
  "expo-sqlite": "*",
21
22
  "prettier": "^3.7.4",
22
23
  "react-native": "0.85.0",
@@ -26,11 +27,12 @@
26
27
  },
27
28
  "dependencies": {
28
29
  "@standard-schema/spec": "^1.0.0",
29
- "@tanstack/react-db": "^0.1.86",
30
- "@tanstack/react-query": "^5.101.0",
31
30
  "nanoid": "5.1.6"
32
31
  },
33
32
  "peerDependencies": {
33
+ "@tanstack/react-db": "^0.1.86",
34
+ "@tanstack/react-query": "^5.101.0",
35
+ "expo-file-system": "^56.0.8",
34
36
  "expo-sqlite": "*",
35
37
  "react": "^19",
36
38
  "react-native": "0.85.x"
@@ -51,6 +53,16 @@
51
53
  "import": "./dist/source.mjs",
52
54
  "require": "./dist/source.cjs"
53
55
  },
56
+ "./settings": {
57
+ "types": "./dist/settings.d.mts",
58
+ "import": "./dist/settings.mjs",
59
+ "require": "./dist/settings.cjs"
60
+ },
61
+ "./media": {
62
+ "types": "./dist/media.d.mts",
63
+ "import": "./dist/media.mjs",
64
+ "require": "./dist/media.cjs"
65
+ },
54
66
  "./package.json": "./package.json"
55
67
  }
56
68
  }