silosdk 0.0.0 → 0.0.1

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 (85) hide show
  1. package/README.md +3 -1
  2. package/dist/_virtual/rolldown_runtime.cjs +29 -0
  3. package/dist/cli/d1.cjs +93 -0
  4. package/dist/cli/d1.mjs +92 -0
  5. package/dist/cli/index.cjs +93 -0
  6. package/dist/cli/index.d.cts +1 -0
  7. package/dist/cli/index.d.mts +1 -0
  8. package/dist/cli/index.mjs +94 -0
  9. package/dist/cli/init.cjs +134 -0
  10. package/dist/cli/init.mjs +133 -0
  11. package/dist/cli/kv.cjs +63 -0
  12. package/dist/cli/kv.mjs +60 -0
  13. package/dist/cli/r2.cjs +83 -0
  14. package/dist/cli/r2.mjs +82 -0
  15. package/dist/cli/wrangler.cjs +93 -0
  16. package/dist/cli/wrangler.mjs +89 -0
  17. package/dist/local/adapters/cloudflare.cjs +200 -0
  18. package/dist/local/adapters/cloudflare.d.cts +50 -0
  19. package/dist/local/adapters/cloudflare.d.mts +50 -0
  20. package/dist/local/adapters/cloudflare.mjs +200 -0
  21. package/dist/local/auth-context.cjs +14 -0
  22. package/dist/local/auth-context.d.cts +7 -0
  23. package/dist/local/auth-context.d.mts +7 -0
  24. package/dist/local/auth-context.mjs +12 -0
  25. package/dist/local/auth.cjs +109 -0
  26. package/dist/local/auth.d.cts +26 -0
  27. package/dist/local/auth.d.mts +26 -0
  28. package/dist/local/auth.mjs +99 -0
  29. package/dist/local/commit.cjs +350 -0
  30. package/dist/local/commit.d.cts +59 -0
  31. package/dist/local/commit.d.mts +59 -0
  32. package/dist/local/commit.mjs +349 -0
  33. package/dist/local/config.cjs +17 -0
  34. package/dist/local/config.mjs +15 -0
  35. package/dist/local/index.cjs +16 -0
  36. package/dist/local/index.d.cts +10 -0
  37. package/dist/local/index.d.mts +10 -0
  38. package/dist/local/index.mjs +9 -0
  39. package/dist/local/provider.cjs +204 -0
  40. package/dist/local/provider.d.cts +25 -0
  41. package/dist/local/provider.d.mts +25 -0
  42. package/dist/local/provider.mjs +203 -0
  43. package/dist/local/query-store.cjs +276 -0
  44. package/dist/local/query-store.mjs +274 -0
  45. package/dist/local/storage.cjs +71 -0
  46. package/dist/local/storage.d.cts +7 -0
  47. package/dist/local/storage.d.mts +7 -0
  48. package/dist/local/storage.mjs +68 -0
  49. package/dist/local/sync.cjs +124 -0
  50. package/dist/local/sync.d.cts +36 -0
  51. package/dist/local/sync.d.mts +36 -0
  52. package/dist/local/sync.mjs +122 -0
  53. package/dist/local/view.cjs +257 -0
  54. package/dist/local/view.d.cts +24 -0
  55. package/dist/local/view.d.mts +24 -0
  56. package/dist/local/view.mjs +254 -0
  57. package/dist/package.cjs +11 -0
  58. package/dist/package.mjs +5 -0
  59. package/dist/schema/index.cjs +276 -0
  60. package/dist/schema/index.d.cts +207 -0
  61. package/dist/schema/index.d.mts +207 -0
  62. package/dist/schema/index.mjs +265 -0
  63. package/dist/server/auth.cjs +132 -0
  64. package/dist/server/auth.d.cts +49 -0
  65. package/dist/server/auth.d.mts +49 -0
  66. package/dist/server/auth.mjs +122 -0
  67. package/dist/server/d1.cjs +120 -0
  68. package/dist/server/d1.mjs +116 -0
  69. package/dist/server/do.cjs +132 -0
  70. package/dist/server/do.d.cts +21 -0
  71. package/dist/server/do.d.mts +21 -0
  72. package/dist/server/do.mjs +131 -0
  73. package/dist/server/index.cjs +355 -0
  74. package/dist/server/index.d.cts +65 -0
  75. package/dist/server/index.d.mts +65 -0
  76. package/dist/server/index.mjs +348 -0
  77. package/dist/server/protect.cjs +34 -0
  78. package/dist/server/protect.d.cts +32 -0
  79. package/dist/server/protect.d.mts +32 -0
  80. package/dist/server/protect.mjs +33 -0
  81. package/dist/server/r2.cjs +58 -0
  82. package/dist/server/r2.d.cts +4 -0
  83. package/dist/server/r2.d.mts +4 -0
  84. package/dist/server/r2.mjs +53 -0
  85. package/package.json +55 -2
@@ -0,0 +1,203 @@
1
+ import { applyRemoteOps } from "./sync.mjs";
2
+ import { CloudflareSyncAdapter } from "./adapters/cloudflare.mjs";
3
+ import { authEvents, clearStoredSessionToken, fetchCurrentUser, getStoredSessionToken, requestOtp, setAuthProdUrl, signOutFromServer, verifyOtpWithServer } from "./auth.mjs";
4
+ import { Context } from "./config.mjs";
5
+ import { getQueryStoreInstance } from "./view.mjs";
6
+ import { AuthContext } from "./auth-context.mjs";
7
+ import { useCallback, useEffect, useRef, useState } from "react";
8
+ import { SQLiteProvider, useSQLiteContext } from "expo-sqlite";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+
11
+ //#region src/local/provider.tsx
12
+ function Provider(props) {
13
+ const { config } = props;
14
+ const subscribers = useRef(/* @__PURE__ */ new Map()).current;
15
+ const url = config?.url;
16
+ const prodUrl = config?.prodUrl;
17
+ const [user, setUser] = useState(null);
18
+ const [isLoading, setIsLoading] = useState(true);
19
+ const [authReady, setAuthReady] = useState(false);
20
+ const tokenRef = useRef(null);
21
+ const authReadyPromiseRef = useRef(null);
22
+ const authReadyResolveRef = useRef(null);
23
+ if (!authReadyPromiseRef.current) authReadyPromiseRef.current = new Promise((resolve) => {
24
+ authReadyResolveRef.current = resolve;
25
+ });
26
+ const resolvedGetToken = config?.getToken ? config.getToken : config?.url ? () => getStoredSessionToken(config?.url) : void 0;
27
+ const dbName = `${config?.name ?? "app"}.db`;
28
+ useEffect(() => {
29
+ setAuthProdUrl(prodUrl ?? null);
30
+ }, [prodUrl]);
31
+ useEffect(() => {
32
+ if (!url) {
33
+ setIsLoading(false);
34
+ setAuthReady(true);
35
+ return;
36
+ }
37
+ (async () => {
38
+ try {
39
+ const token = await getStoredSessionToken(url);
40
+ if (token) {
41
+ tokenRef.current = token;
42
+ const current = await fetchCurrentUser(url, token);
43
+ if (current.user) setUser(current.user);
44
+ else {
45
+ await clearStoredSessionToken(url);
46
+ tokenRef.current = null;
47
+ }
48
+ }
49
+ } catch (err) {
50
+ console.error("[silosdk] Auth init failed:", err);
51
+ } finally {
52
+ setIsLoading(false);
53
+ setAuthReady(true);
54
+ }
55
+ })();
56
+ }, [url]);
57
+ useEffect(() => {
58
+ if (authReady) authReadyResolveRef.current?.();
59
+ }, [authReady]);
60
+ const signIn = useCallback(async (opts) => {
61
+ if (!url) throw new Error("No URL configured");
62
+ await requestOtp(url, opts.email);
63
+ }, [url]);
64
+ const verifyOtp = useCallback(async (opts) => {
65
+ if (!url) throw new Error("No URL configured");
66
+ const result = await verifyOtpWithServer(url, opts);
67
+ tokenRef.current = result.token;
68
+ setUser(result.user);
69
+ authEvents.emit("token-changed");
70
+ }, [url]);
71
+ const signOut = useCallback(async () => {
72
+ if (!url) return;
73
+ const token = tokenRef.current;
74
+ if (token) await signOutFromServer(url, token);
75
+ await clearStoredSessionToken(url);
76
+ tokenRef.current = null;
77
+ setUser(null);
78
+ authEvents.emit("token-changed");
79
+ }, [url]);
80
+ useEffect(() => {
81
+ if (!url) return;
82
+ const handleUnauthorized = () => {
83
+ tokenRef.current = null;
84
+ setUser(null);
85
+ };
86
+ authEvents.on("unauthorized", handleUnauthorized);
87
+ return () => authEvents.off("unauthorized", handleUnauthorized);
88
+ }, [url]);
89
+ const authState = {
90
+ user,
91
+ isLoading,
92
+ signIn,
93
+ verifyOtp,
94
+ signOut
95
+ };
96
+ return /* @__PURE__ */ jsx(Context.Provider, {
97
+ value: {
98
+ subscribers,
99
+ url: config?.url,
100
+ getToken: resolvedGetToken,
101
+ authReady,
102
+ authReadyPromise: authReadyPromiseRef.current ?? Promise.resolve()
103
+ },
104
+ children: /* @__PURE__ */ jsx(AuthContext.Provider, {
105
+ value: authState,
106
+ children: /* @__PURE__ */ jsxs(SQLiteProvider, {
107
+ databaseName: dbName,
108
+ onInit: async (db) => {
109
+ await db.execAsync(`
110
+ PRAGMA journal_mode = WAL;
111
+ CREATE TABLE IF NOT EXISTS views (
112
+ id TEXT PRIMARY KEY,
113
+ view TEXT,
114
+ data BLOB,
115
+ createdAt TEXT,
116
+ updatedAt TEXT,
117
+ version INTEGER DEFAULT 0
118
+ );
119
+ CREATE TABLE IF NOT EXISTS relations (
120
+ parent TEXT,
121
+ pid TEXT,
122
+ child TEXT,
123
+ cid TEXT
124
+ );
125
+ `);
126
+ },
127
+ options: { enableChangeListener: true },
128
+ children: [/* @__PURE__ */ jsx(SyncManagerWithConfig, {
129
+ config,
130
+ subscribers
131
+ }), props.children]
132
+ })
133
+ })
134
+ });
135
+ }
136
+ function SyncManagerWithConfig({ config, subscribers }) {
137
+ const db = useSQLiteContext();
138
+ const adapterRef = useRef(void 0);
139
+ const [isAuthenticated, setIsAuthenticated] = useState(null);
140
+ const checkAuth = useCallback(async () => {
141
+ if (!config?.url) {
142
+ setIsAuthenticated(false);
143
+ return;
144
+ }
145
+ const token = await (config.getToken ? config.getToken : () => getStoredSessionToken(config.url))();
146
+ if (!token) {
147
+ setIsAuthenticated(false);
148
+ return;
149
+ }
150
+ try {
151
+ const res = await fetch(`${config.url}/auth/me`, { headers: { Authorization: `Bearer ${token}` } });
152
+ if (res.ok) setIsAuthenticated(!(await res.json()).isAnonymous);
153
+ else setIsAuthenticated(false);
154
+ } catch {
155
+ setIsAuthenticated(false);
156
+ }
157
+ }, [config?.url, config?.getToken]);
158
+ useEffect(() => {
159
+ checkAuth();
160
+ }, [checkAuth]);
161
+ useEffect(() => {
162
+ const handleTokenChanged = () => {
163
+ getQueryStoreInstance().clear();
164
+ checkAuth();
165
+ adapterRef.current?.resetConnection();
166
+ };
167
+ authEvents.on("token-changed", handleTokenChanged);
168
+ return () => {
169
+ authEvents.off("token-changed", handleTokenChanged);
170
+ };
171
+ }, [checkAuth]);
172
+ useEffect(() => {
173
+ if (!config?.url || isAuthenticated !== true) return;
174
+ const resolvedGetToken = config.getToken ? config.getToken : () => getStoredSessionToken(config.url);
175
+ const adapter = new CloudflareSyncAdapter({
176
+ url: config.url,
177
+ getToken: resolvedGetToken,
178
+ db,
179
+ subscribers
180
+ });
181
+ adapterRef.current = adapter;
182
+ let mounted = true;
183
+ adapter.onReceive(async (payload) => {
184
+ if (mounted) await applyRemoteOps(db, payload.ops, subscribers, adapter.merge);
185
+ });
186
+ adapter.connect();
187
+ return () => {
188
+ mounted = false;
189
+ adapter.disconnect();
190
+ adapterRef.current = void 0;
191
+ };
192
+ }, [
193
+ config?.url,
194
+ config?.getToken,
195
+ db,
196
+ subscribers,
197
+ isAuthenticated
198
+ ]);
199
+ return null;
200
+ }
201
+
202
+ //#endregion
203
+ export { Provider };
@@ -0,0 +1,276 @@
1
+
2
+ //#region src/local/query-store.ts
3
+ const RETRY_WINDOW_MS = 3e4;
4
+ const MAX_RETRIES = 5;
5
+ var QueryStore = class {
6
+ constructor() {
7
+ this.cache = /* @__PURE__ */ new Map();
8
+ }
9
+ classifyError(error) {
10
+ if (error instanceof Error) {
11
+ if (error.message.includes("Unauthorized")) return {
12
+ retryable: false,
13
+ reason: "unauthorized"
14
+ };
15
+ if (error.message.includes("Forbidden")) return {
16
+ retryable: false,
17
+ reason: "forbidden"
18
+ };
19
+ }
20
+ return {
21
+ retryable: true,
22
+ reason: "unknown"
23
+ };
24
+ }
25
+ getRetryState(entry) {
26
+ if (!entry.retry) entry.retry = {
27
+ count: 0,
28
+ firstFailureAt: 0,
29
+ blocked: false,
30
+ warned: false
31
+ };
32
+ return entry.retry;
33
+ }
34
+ resetRetry(entry) {
35
+ if (entry.retry) {
36
+ entry.retry.count = 0;
37
+ entry.retry.firstFailureAt = 0;
38
+ entry.retry.blocked = false;
39
+ entry.retry.warned = false;
40
+ }
41
+ }
42
+ shouldRetry(key, entry, error) {
43
+ const classification = this.classifyError(error);
44
+ if (!classification.retryable) {
45
+ const retry$1 = this.getRetryState(entry);
46
+ retry$1.blocked = true;
47
+ if (!retry$1.warned) {
48
+ retry$1.warned = true;
49
+ console.warn(`[QueryStore] Blocking retries for ${key}: ${classification.reason}`);
50
+ }
51
+ return false;
52
+ }
53
+ const retry = this.getRetryState(entry);
54
+ const now = Date.now();
55
+ if (retry.blocked) return false;
56
+ if (!retry.firstFailureAt || now - retry.firstFailureAt > RETRY_WINDOW_MS) {
57
+ retry.firstFailureAt = now;
58
+ retry.count = 0;
59
+ }
60
+ retry.count += 1;
61
+ if (retry.count > MAX_RETRIES) {
62
+ retry.blocked = true;
63
+ if (!retry.warned) {
64
+ retry.warned = true;
65
+ console.warn(`[QueryStore] Retry budget exceeded for ${key}`);
66
+ }
67
+ return false;
68
+ }
69
+ return true;
70
+ }
71
+ /**
72
+ * Ensure data is being fetched for a cache key.
73
+ * Safe to call multiple times - only triggers one fetch.
74
+ * Call this during render before useSyncExternalStore.
75
+ */
76
+ ensureFetching(key, fetcher, getToken) {
77
+ let entry = this.cache.get(key);
78
+ if (entry?.promise || entry?.status === "resolved") return;
79
+ if (entry?.retry?.blocked) return;
80
+ if (!entry) {
81
+ entry = {
82
+ status: "pending",
83
+ listeners: /* @__PURE__ */ new Set()
84
+ };
85
+ this.cache.set(key, entry);
86
+ }
87
+ const promise = fetcher().then((data) => {
88
+ const current = this.cache.get(key);
89
+ if (current) {
90
+ current.status = "resolved";
91
+ current.data = data;
92
+ current.error = void 0;
93
+ current.promise = void 0;
94
+ this.resetRetry(current);
95
+ this.notify(key);
96
+ }
97
+ return data;
98
+ }).catch((error) => {
99
+ const current = this.cache.get(key);
100
+ if (current) if (this.shouldRetry(key, current, error)) {
101
+ current.status = "rejected";
102
+ current.error = error;
103
+ current.promise = void 0;
104
+ this.notify(key);
105
+ } else {
106
+ current.status = "rejected";
107
+ current.error = error;
108
+ current.promise = void 0;
109
+ this.notify(key);
110
+ }
111
+ throw error;
112
+ });
113
+ entry.status = "pending";
114
+ entry.promise = promise;
115
+ }
116
+ /**
117
+ * Subscribe to changes for a specific cache key.
118
+ * Returns an unsubscribe function.
119
+ */
120
+ subscribe(key, callback) {
121
+ let entry = this.cache.get(key);
122
+ if (!entry) {
123
+ entry = {
124
+ status: "pending",
125
+ listeners: /* @__PURE__ */ new Set()
126
+ };
127
+ this.cache.set(key, entry);
128
+ }
129
+ entry.listeners.add(callback);
130
+ return () => {
131
+ entry.listeners.delete(callback);
132
+ if (entry.listeners.size === 0 && entry.status === "pending" && !entry.promise) this.cache.delete(key);
133
+ };
134
+ }
135
+ /**
136
+ * Get the current snapshot for a cache key.
137
+ * Throws a Promise if data is still loading (for Suspense).
138
+ * Throws the error if the fetch failed.
139
+ * This is a pure function - no side effects.
140
+ */
141
+ getSnapshot(key) {
142
+ const entry = this.cache.get(key);
143
+ if (!entry) throw new Promise(() => {});
144
+ if (entry.status === "pending") {
145
+ if (entry.promise) throw entry.promise;
146
+ throw new Promise(() => {});
147
+ }
148
+ if (entry.status === "rejected") throw entry.error;
149
+ return entry.data;
150
+ }
151
+ /**
152
+ * Check if a key has resolved data (useful for avoiding Suspense in some cases)
153
+ */
154
+ has(key) {
155
+ return this.cache.get(key)?.status === "resolved";
156
+ }
157
+ /**
158
+ * Get data if available, otherwise return undefined.
159
+ * Does not throw - useful for optional data or checking cache.
160
+ */
161
+ peek(key) {
162
+ const entry = this.cache.get(key);
163
+ if (entry?.status === "resolved") return entry.data;
164
+ }
165
+ /**
166
+ * Refetch data for a cache key, even if it's already resolved.
167
+ * Keeps old data visible while reloading (no Suspense trigger).
168
+ */
169
+ refetch(key, fetcher, getToken) {
170
+ const entry = this.cache.get(key);
171
+ if (entry?.promise) return entry.promise;
172
+ const promise = fetcher().then((data) => {
173
+ const current = this.cache.get(key);
174
+ if (current) {
175
+ current.status = "resolved";
176
+ current.data = data;
177
+ current.error = void 0;
178
+ current.promise = void 0;
179
+ this.resetRetry(current);
180
+ this.notify(key);
181
+ }
182
+ return data;
183
+ }).catch((error) => {
184
+ const current = this.cache.get(key);
185
+ if (current) if (this.shouldRetry(key, current, error)) {
186
+ current.status = "rejected";
187
+ current.error = error;
188
+ current.promise = void 0;
189
+ this.notify(key);
190
+ } else {
191
+ current.status = "rejected";
192
+ current.error = error;
193
+ current.promise = void 0;
194
+ this.notify(key);
195
+ }
196
+ throw error;
197
+ });
198
+ if (entry) {
199
+ this.resetRetry(entry);
200
+ entry.promise = promise;
201
+ } else {
202
+ const newEntry = {
203
+ status: "pending",
204
+ promise,
205
+ listeners: /* @__PURE__ */ new Set()
206
+ };
207
+ this.cache.set(key, newEntry);
208
+ }
209
+ return promise;
210
+ }
211
+ /**
212
+ * Invalidate cache entries that match any of the given view names.
213
+ * Keys are expected to be in format "viewName:serializedOptions"
214
+ * Marks entries as stale so next ensureFetching will refetch.
215
+ */
216
+ invalidate(viewNames) {
217
+ for (const [key, entry] of this.cache.entries()) {
218
+ const viewName = key.split(":")[0];
219
+ if (viewNames.has(viewName)) {
220
+ if (entry.status === "resolved") {
221
+ entry.status = "pending";
222
+ entry.data = void 0;
223
+ entry.promise = void 0;
224
+ this.resetRetry(entry);
225
+ this.notify(key);
226
+ }
227
+ }
228
+ }
229
+ }
230
+ /**
231
+ * Invalidate a specific cache key
232
+ */
233
+ invalidateKey(key) {
234
+ const entry = this.cache.get(key);
235
+ if (entry && entry.status === "resolved") {
236
+ entry.status = "pending";
237
+ entry.data = void 0;
238
+ entry.promise = void 0;
239
+ this.resetRetry(entry);
240
+ this.notify(key);
241
+ }
242
+ }
243
+ /**
244
+ * Clear all cache entries
245
+ */
246
+ clear() {
247
+ for (const key of this.cache.keys()) this.notify(key);
248
+ this.cache.clear();
249
+ }
250
+ /**
251
+ * Notify all listeners for a cache key
252
+ */
253
+ notify(key) {
254
+ const entry = this.cache.get(key);
255
+ if (entry) for (const listener of entry.listeners) listener();
256
+ }
257
+ };
258
+ /**
259
+ * Create a stable cache key from view name and options
260
+ */
261
+ function createCacheKey(viewName, options) {
262
+ return `${viewName}:${stableStringify(options)}`;
263
+ }
264
+ /**
265
+ * Stable JSON stringify that sorts object keys
266
+ */
267
+ function stableStringify(obj) {
268
+ if (obj === null || obj === void 0) return "";
269
+ if (typeof obj !== "object") return JSON.stringify(obj);
270
+ if (Array.isArray(obj)) return "[" + obj.map(stableStringify).join(",") + "]";
271
+ return "{" + Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",") + "}";
272
+ }
273
+
274
+ //#endregion
275
+ exports.QueryStore = QueryStore;
276
+ exports.createCacheKey = createCacheKey;