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,274 @@
1
+ //#region src/local/query-store.ts
2
+ const RETRY_WINDOW_MS = 3e4;
3
+ const MAX_RETRIES = 5;
4
+ var QueryStore = class {
5
+ constructor() {
6
+ this.cache = /* @__PURE__ */ new Map();
7
+ }
8
+ classifyError(error) {
9
+ if (error instanceof Error) {
10
+ if (error.message.includes("Unauthorized")) return {
11
+ retryable: false,
12
+ reason: "unauthorized"
13
+ };
14
+ if (error.message.includes("Forbidden")) return {
15
+ retryable: false,
16
+ reason: "forbidden"
17
+ };
18
+ }
19
+ return {
20
+ retryable: true,
21
+ reason: "unknown"
22
+ };
23
+ }
24
+ getRetryState(entry) {
25
+ if (!entry.retry) entry.retry = {
26
+ count: 0,
27
+ firstFailureAt: 0,
28
+ blocked: false,
29
+ warned: false
30
+ };
31
+ return entry.retry;
32
+ }
33
+ resetRetry(entry) {
34
+ if (entry.retry) {
35
+ entry.retry.count = 0;
36
+ entry.retry.firstFailureAt = 0;
37
+ entry.retry.blocked = false;
38
+ entry.retry.warned = false;
39
+ }
40
+ }
41
+ shouldRetry(key, entry, error) {
42
+ const classification = this.classifyError(error);
43
+ if (!classification.retryable) {
44
+ const retry$1 = this.getRetryState(entry);
45
+ retry$1.blocked = true;
46
+ if (!retry$1.warned) {
47
+ retry$1.warned = true;
48
+ console.warn(`[QueryStore] Blocking retries for ${key}: ${classification.reason}`);
49
+ }
50
+ return false;
51
+ }
52
+ const retry = this.getRetryState(entry);
53
+ const now = Date.now();
54
+ if (retry.blocked) return false;
55
+ if (!retry.firstFailureAt || now - retry.firstFailureAt > RETRY_WINDOW_MS) {
56
+ retry.firstFailureAt = now;
57
+ retry.count = 0;
58
+ }
59
+ retry.count += 1;
60
+ if (retry.count > MAX_RETRIES) {
61
+ retry.blocked = true;
62
+ if (!retry.warned) {
63
+ retry.warned = true;
64
+ console.warn(`[QueryStore] Retry budget exceeded for ${key}`);
65
+ }
66
+ return false;
67
+ }
68
+ return true;
69
+ }
70
+ /**
71
+ * Ensure data is being fetched for a cache key.
72
+ * Safe to call multiple times - only triggers one fetch.
73
+ * Call this during render before useSyncExternalStore.
74
+ */
75
+ ensureFetching(key, fetcher, getToken) {
76
+ let entry = this.cache.get(key);
77
+ if (entry?.promise || entry?.status === "resolved") return;
78
+ if (entry?.retry?.blocked) return;
79
+ if (!entry) {
80
+ entry = {
81
+ status: "pending",
82
+ listeners: /* @__PURE__ */ new Set()
83
+ };
84
+ this.cache.set(key, entry);
85
+ }
86
+ const promise = fetcher().then((data) => {
87
+ const current = this.cache.get(key);
88
+ if (current) {
89
+ current.status = "resolved";
90
+ current.data = data;
91
+ current.error = void 0;
92
+ current.promise = void 0;
93
+ this.resetRetry(current);
94
+ this.notify(key);
95
+ }
96
+ return data;
97
+ }).catch((error) => {
98
+ const current = this.cache.get(key);
99
+ if (current) if (this.shouldRetry(key, current, error)) {
100
+ current.status = "rejected";
101
+ current.error = error;
102
+ current.promise = void 0;
103
+ this.notify(key);
104
+ } else {
105
+ current.status = "rejected";
106
+ current.error = error;
107
+ current.promise = void 0;
108
+ this.notify(key);
109
+ }
110
+ throw error;
111
+ });
112
+ entry.status = "pending";
113
+ entry.promise = promise;
114
+ }
115
+ /**
116
+ * Subscribe to changes for a specific cache key.
117
+ * Returns an unsubscribe function.
118
+ */
119
+ subscribe(key, callback) {
120
+ let entry = this.cache.get(key);
121
+ if (!entry) {
122
+ entry = {
123
+ status: "pending",
124
+ listeners: /* @__PURE__ */ new Set()
125
+ };
126
+ this.cache.set(key, entry);
127
+ }
128
+ entry.listeners.add(callback);
129
+ return () => {
130
+ entry.listeners.delete(callback);
131
+ if (entry.listeners.size === 0 && entry.status === "pending" && !entry.promise) this.cache.delete(key);
132
+ };
133
+ }
134
+ /**
135
+ * Get the current snapshot for a cache key.
136
+ * Throws a Promise if data is still loading (for Suspense).
137
+ * Throws the error if the fetch failed.
138
+ * This is a pure function - no side effects.
139
+ */
140
+ getSnapshot(key) {
141
+ const entry = this.cache.get(key);
142
+ if (!entry) throw new Promise(() => {});
143
+ if (entry.status === "pending") {
144
+ if (entry.promise) throw entry.promise;
145
+ throw new Promise(() => {});
146
+ }
147
+ if (entry.status === "rejected") throw entry.error;
148
+ return entry.data;
149
+ }
150
+ /**
151
+ * Check if a key has resolved data (useful for avoiding Suspense in some cases)
152
+ */
153
+ has(key) {
154
+ return this.cache.get(key)?.status === "resolved";
155
+ }
156
+ /**
157
+ * Get data if available, otherwise return undefined.
158
+ * Does not throw - useful for optional data or checking cache.
159
+ */
160
+ peek(key) {
161
+ const entry = this.cache.get(key);
162
+ if (entry?.status === "resolved") return entry.data;
163
+ }
164
+ /**
165
+ * Refetch data for a cache key, even if it's already resolved.
166
+ * Keeps old data visible while reloading (no Suspense trigger).
167
+ */
168
+ refetch(key, fetcher, getToken) {
169
+ const entry = this.cache.get(key);
170
+ if (entry?.promise) return entry.promise;
171
+ const promise = fetcher().then((data) => {
172
+ const current = this.cache.get(key);
173
+ if (current) {
174
+ current.status = "resolved";
175
+ current.data = data;
176
+ current.error = void 0;
177
+ current.promise = void 0;
178
+ this.resetRetry(current);
179
+ this.notify(key);
180
+ }
181
+ return data;
182
+ }).catch((error) => {
183
+ const current = this.cache.get(key);
184
+ if (current) if (this.shouldRetry(key, current, error)) {
185
+ current.status = "rejected";
186
+ current.error = error;
187
+ current.promise = void 0;
188
+ this.notify(key);
189
+ } else {
190
+ current.status = "rejected";
191
+ current.error = error;
192
+ current.promise = void 0;
193
+ this.notify(key);
194
+ }
195
+ throw error;
196
+ });
197
+ if (entry) {
198
+ this.resetRetry(entry);
199
+ entry.promise = promise;
200
+ } else {
201
+ const newEntry = {
202
+ status: "pending",
203
+ promise,
204
+ listeners: /* @__PURE__ */ new Set()
205
+ };
206
+ this.cache.set(key, newEntry);
207
+ }
208
+ return promise;
209
+ }
210
+ /**
211
+ * Invalidate cache entries that match any of the given view names.
212
+ * Keys are expected to be in format "viewName:serializedOptions"
213
+ * Marks entries as stale so next ensureFetching will refetch.
214
+ */
215
+ invalidate(viewNames) {
216
+ for (const [key, entry] of this.cache.entries()) {
217
+ const viewName = key.split(":")[0];
218
+ if (viewNames.has(viewName)) {
219
+ if (entry.status === "resolved") {
220
+ entry.status = "pending";
221
+ entry.data = void 0;
222
+ entry.promise = void 0;
223
+ this.resetRetry(entry);
224
+ this.notify(key);
225
+ }
226
+ }
227
+ }
228
+ }
229
+ /**
230
+ * Invalidate a specific cache key
231
+ */
232
+ invalidateKey(key) {
233
+ const entry = this.cache.get(key);
234
+ if (entry && entry.status === "resolved") {
235
+ entry.status = "pending";
236
+ entry.data = void 0;
237
+ entry.promise = void 0;
238
+ this.resetRetry(entry);
239
+ this.notify(key);
240
+ }
241
+ }
242
+ /**
243
+ * Clear all cache entries
244
+ */
245
+ clear() {
246
+ for (const key of this.cache.keys()) this.notify(key);
247
+ this.cache.clear();
248
+ }
249
+ /**
250
+ * Notify all listeners for a cache key
251
+ */
252
+ notify(key) {
253
+ const entry = this.cache.get(key);
254
+ if (entry) for (const listener of entry.listeners) listener();
255
+ }
256
+ };
257
+ /**
258
+ * Create a stable cache key from view name and options
259
+ */
260
+ function createCacheKey(viewName, options) {
261
+ return `${viewName}:${stableStringify(options)}`;
262
+ }
263
+ /**
264
+ * Stable JSON stringify that sorts object keys
265
+ */
266
+ function stableStringify(obj) {
267
+ if (obj === null || obj === void 0) return "";
268
+ if (typeof obj !== "object") return JSON.stringify(obj);
269
+ if (Array.isArray(obj)) return "[" + obj.map(stableStringify).join(",") + "]";
270
+ return "{" + Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",") + "}";
271
+ }
272
+
273
+ //#endregion
274
+ export { QueryStore, createCacheKey };
@@ -0,0 +1,71 @@
1
+ const require_config = require('./config.cjs');
2
+
3
+ //#region src/local/storage.ts
4
+ function isPendingAsset(value) {
5
+ return typeof value === "object" && value !== null && typeof value.uri === "string" && value.uri.length > 0;
6
+ }
7
+ function isResolvedAsset(value) {
8
+ return typeof value === "object" && value !== null && typeof value.key === "string" && value.key.length > 0;
9
+ }
10
+ function isAssetValue(value) {
11
+ return isPendingAsset(value) || isResolvedAsset(value);
12
+ }
13
+ function assetUrl(baseUrl, asset) {
14
+ if (!asset) return null;
15
+ if (isPendingAsset(asset)) return asset.uri;
16
+ if (isResolvedAsset(asset)) return `${baseUrl}/storage/${encodeURIComponent(asset.key)}`;
17
+ return null;
18
+ }
19
+ function useAssetUrl(asset) {
20
+ const { url } = require_config.useConfig();
21
+ if (!url) return isPendingAsset(asset) ? asset.uri : null;
22
+ return assetUrl(url, asset);
23
+ }
24
+ async function uploadPendingAsset(input) {
25
+ const { pending, scope, url, getToken } = input;
26
+ const sourceRes = await fetch(pending.uri);
27
+ const blob = await sourceRes.blob();
28
+ if (!blob) throw new Error(`Failed to read asset from uri: ${pending.uri}`);
29
+ const contentType = pending.contentType || blob.type || sourceRes.headers.get("Content-Type") || "application/octet-stream";
30
+ const size = pending.size ?? blob.size;
31
+ const headers = {
32
+ "Content-Type": contentType,
33
+ "X-Asset-Scope": scope
34
+ };
35
+ if (pending.name) headers["X-File-Name"] = pending.name;
36
+ if (typeof size === "number") headers["Content-Length"] = String(size);
37
+ if (getToken) {
38
+ const token = await getToken();
39
+ if (token) headers.Authorization = `Bearer ${token}`;
40
+ }
41
+ const uploadRes = await fetch(`${url}/storage/upload`, {
42
+ method: "POST",
43
+ headers,
44
+ body: blob
45
+ });
46
+ if (!uploadRes.ok) throw new Error(`Asset upload failed: ${uploadRes.status}`);
47
+ const ref = await uploadRes.json();
48
+ if (!isResolvedAsset(ref)) throw new Error("Asset upload returned invalid payload");
49
+ return {
50
+ key: ref.key,
51
+ name: ref.name ?? pending.name,
52
+ contentType: ref.contentType ?? contentType,
53
+ size: ref.size ?? size
54
+ };
55
+ }
56
+ async function resolvePendingAsset(value, scope, url, getToken) {
57
+ if (!isPendingAsset(value)) return value;
58
+ if (!url) return value;
59
+ return uploadPendingAsset({
60
+ pending: value,
61
+ scope,
62
+ url,
63
+ getToken
64
+ });
65
+ }
66
+
67
+ //#endregion
68
+ exports.isAssetValue = isAssetValue;
69
+ exports.isResolvedAsset = isResolvedAsset;
70
+ exports.resolvePendingAsset = resolvePendingAsset;
71
+ exports.useAssetUrl = useAssetUrl;
@@ -0,0 +1,7 @@
1
+ import { AssetValue } from "../schema/index.cjs";
2
+
3
+ //#region src/local/storage.d.ts
4
+
5
+ declare function useAssetUrl(asset: AssetValue | null | undefined): string | null;
6
+ //#endregion
7
+ export { useAssetUrl };
@@ -0,0 +1,7 @@
1
+ import { AssetValue } from "../schema/index.mjs";
2
+
3
+ //#region src/local/storage.d.ts
4
+
5
+ declare function useAssetUrl(asset: AssetValue | null | undefined): string | null;
6
+ //#endregion
7
+ export { useAssetUrl };
@@ -0,0 +1,68 @@
1
+ import { useConfig } from "./config.mjs";
2
+
3
+ //#region src/local/storage.ts
4
+ function isPendingAsset(value) {
5
+ return typeof value === "object" && value !== null && typeof value.uri === "string" && value.uri.length > 0;
6
+ }
7
+ function isResolvedAsset(value) {
8
+ return typeof value === "object" && value !== null && typeof value.key === "string" && value.key.length > 0;
9
+ }
10
+ function isAssetValue(value) {
11
+ return isPendingAsset(value) || isResolvedAsset(value);
12
+ }
13
+ function assetUrl(baseUrl, asset) {
14
+ if (!asset) return null;
15
+ if (isPendingAsset(asset)) return asset.uri;
16
+ if (isResolvedAsset(asset)) return `${baseUrl}/storage/${encodeURIComponent(asset.key)}`;
17
+ return null;
18
+ }
19
+ function useAssetUrl(asset) {
20
+ const { url } = useConfig();
21
+ if (!url) return isPendingAsset(asset) ? asset.uri : null;
22
+ return assetUrl(url, asset);
23
+ }
24
+ async function uploadPendingAsset(input) {
25
+ const { pending, scope, url, getToken } = input;
26
+ const sourceRes = await fetch(pending.uri);
27
+ const blob = await sourceRes.blob();
28
+ if (!blob) throw new Error(`Failed to read asset from uri: ${pending.uri}`);
29
+ const contentType = pending.contentType || blob.type || sourceRes.headers.get("Content-Type") || "application/octet-stream";
30
+ const size = pending.size ?? blob.size;
31
+ const headers = {
32
+ "Content-Type": contentType,
33
+ "X-Asset-Scope": scope
34
+ };
35
+ if (pending.name) headers["X-File-Name"] = pending.name;
36
+ if (typeof size === "number") headers["Content-Length"] = String(size);
37
+ if (getToken) {
38
+ const token = await getToken();
39
+ if (token) headers.Authorization = `Bearer ${token}`;
40
+ }
41
+ const uploadRes = await fetch(`${url}/storage/upload`, {
42
+ method: "POST",
43
+ headers,
44
+ body: blob
45
+ });
46
+ if (!uploadRes.ok) throw new Error(`Asset upload failed: ${uploadRes.status}`);
47
+ const ref = await uploadRes.json();
48
+ if (!isResolvedAsset(ref)) throw new Error("Asset upload returned invalid payload");
49
+ return {
50
+ key: ref.key,
51
+ name: ref.name ?? pending.name,
52
+ contentType: ref.contentType ?? contentType,
53
+ size: ref.size ?? size
54
+ };
55
+ }
56
+ async function resolvePendingAsset(value, scope, url, getToken) {
57
+ if (!isPendingAsset(value)) return value;
58
+ if (!url) return value;
59
+ return uploadPendingAsset({
60
+ pending: value,
61
+ scope,
62
+ url,
63
+ getToken
64
+ });
65
+ }
66
+
67
+ //#endregion
68
+ export { isAssetValue, isResolvedAsset, resolvePendingAsset, useAssetUrl };
@@ -0,0 +1,124 @@
1
+
2
+ //#region src/local/sync.ts
3
+ /**
4
+ * Abstract sync adapter base class.
5
+ */
6
+ var SyncAdapter = class {
7
+ constructor(options) {
8
+ this.merge = options?.merge;
9
+ }
10
+ };
11
+ /**
12
+ * Apply remote ops to local SQLite with LWW conflict resolution.
13
+ */
14
+ async function applyRemoteOps(db, ops, subscribers, merge) {
15
+ const touchedViews = /* @__PURE__ */ new Set();
16
+ await db.withTransactionAsync(async () => {
17
+ for (const op of ops) if (op.kind === "add") {
18
+ await handleRemoteAdd(db, op, merge);
19
+ touchedViews.add(op.view.name);
20
+ } else if (op.kind === "update") {
21
+ await handleRemoteUpdate(db, op, merge);
22
+ touchedViews.add(op.view.name);
23
+ } else if (op.kind === "remove") {
24
+ await handleRemoteRemove(db, op);
25
+ touchedViews.add(op.view.name);
26
+ } else if (op.kind === "link" || op.kind === "unlink") {
27
+ await handleRemoteLink(db, op);
28
+ touchedViews.add(op.parent.view.name);
29
+ touchedViews.add(op.child.view.name);
30
+ }
31
+ });
32
+ for (const [key, sub] of subscribers) for (const viewName of touchedViews) if (key.includes(viewName)) {
33
+ sub.update();
34
+ break;
35
+ }
36
+ }
37
+ async function handleRemoteAdd(db, op, merge) {
38
+ const existing = await db.getFirstAsync(`SELECT version, data FROM views WHERE id = ? AND view = ?`, [op.id, op.view.name]);
39
+ if (!existing) await db.runAsync(`INSERT INTO views (id, view, data, createdAt, updatedAt, version)
40
+ VALUES (?, ?, ?, ?, ?, ?)`, [
41
+ op.id,
42
+ op.view.name,
43
+ JSON.stringify(op.value),
44
+ op.timestamp,
45
+ op.timestamp,
46
+ op.version
47
+ ]);
48
+ else if (op.version > existing.version) {
49
+ const finalData = merge ? merge({
50
+ data: JSON.parse(existing.data),
51
+ version: existing.version
52
+ }, {
53
+ data: op.value,
54
+ version: op.version
55
+ }, op.view.name) : op.value;
56
+ await db.runAsync(`UPDATE views SET data = ?, updatedAt = ?, version = ? WHERE id = ? AND view = ?`, [
57
+ JSON.stringify(finalData),
58
+ op.timestamp,
59
+ op.version,
60
+ op.id,
61
+ op.view.name
62
+ ]);
63
+ }
64
+ }
65
+ async function handleRemoteUpdate(db, op, merge) {
66
+ const existing = await db.getFirstAsync(`SELECT version, data FROM views WHERE id = ? AND view = ?`, [op.id, op.view.name]);
67
+ if (!existing) return;
68
+ if (op.version > existing.version) {
69
+ const localData = JSON.parse(existing.data);
70
+ const mergedData = merge ? merge({
71
+ data: localData,
72
+ version: existing.version
73
+ }, {
74
+ data: {
75
+ ...localData,
76
+ ...op.value
77
+ },
78
+ version: op.version
79
+ }, op.view.name) : {
80
+ ...localData,
81
+ ...op.value
82
+ };
83
+ await db.runAsync(`UPDATE views SET data = ?, updatedAt = ?, version = ? WHERE id = ? AND view = ?`, [
84
+ JSON.stringify(mergedData),
85
+ op.timestamp,
86
+ op.version,
87
+ op.id,
88
+ op.view.name
89
+ ]);
90
+ }
91
+ }
92
+ async function handleRemoteRemove(db, op) {
93
+ await db.runAsync(`DELETE FROM views WHERE id = ? AND view = ?`, [op.id, op.view.name]);
94
+ await db.runAsync(`DELETE FROM relations WHERE (parent = ? AND pid = ?) OR (child = ? AND cid = ?)`, [
95
+ op.view.name,
96
+ op.id,
97
+ op.view.name,
98
+ op.id
99
+ ]);
100
+ }
101
+ async function handleRemoteLink(db, op) {
102
+ if (op.kind === "link") {
103
+ if (!await db.getFirstAsync(`SELECT 1 FROM relations WHERE parent = ? AND pid = ? AND child = ? AND cid = ?`, [
104
+ op.parent.view.name,
105
+ op.parent.id,
106
+ op.child.view.name,
107
+ op.child.id
108
+ ])) await db.runAsync(`INSERT INTO relations (parent, pid, child, cid) VALUES (?, ?, ?, ?)`, [
109
+ op.parent.view.name,
110
+ op.parent.id,
111
+ op.child.view.name,
112
+ op.child.id
113
+ ]);
114
+ } else await db.runAsync(`DELETE FROM relations WHERE parent = ? AND pid = ? AND child = ? AND cid = ?`, [
115
+ op.parent.view.name,
116
+ op.parent.id,
117
+ op.child.view.name,
118
+ op.child.id
119
+ ]);
120
+ }
121
+
122
+ //#endregion
123
+ exports.SyncAdapter = SyncAdapter;
124
+ exports.applyRemoteOps = applyRemoteOps;
@@ -0,0 +1,36 @@
1
+ import { View } from "../schema/index.cjs";
2
+ import "./view.cjs";
3
+ import { AddOp, LinkOp, RemoveOp, UpdateOp } from "./commit.cjs";
4
+ import { SQLiteDatabase } from "expo-sqlite";
5
+
6
+ //#region src/local/sync.d.ts
7
+ type Op = AddOp | UpdateOp | RemoveOp | LinkOp<View, View>;
8
+ type SyncOp = Op & {
9
+ version: number;
10
+ timestamp: string;
11
+ };
12
+ type SyncPayload = {
13
+ ops: SyncOp[];
14
+ };
15
+ type MergeFn = (local: {
16
+ data: any;
17
+ version: number;
18
+ }, remote: {
19
+ data: any;
20
+ version: number;
21
+ }, viewName: string) => any;
22
+ /**
23
+ * Abstract sync adapter base class.
24
+ */
25
+ declare abstract class SyncAdapter {
26
+ merge?: MergeFn;
27
+ constructor(options?: {
28
+ merge?: MergeFn;
29
+ });
30
+ abstract send(payload: SyncPayload): Promise<void>;
31
+ abstract onReceive(handler: (payload: SyncPayload) => Promise<void>): void;
32
+ abstract fetchInitialState(): Promise<SyncPayload | null>;
33
+ abstract disconnect(): void;
34
+ }
35
+ //#endregion
36
+ export { MergeFn, SyncAdapter, SyncOp, SyncPayload };
@@ -0,0 +1,36 @@
1
+ import { View } from "../schema/index.mjs";
2
+ import "./view.mjs";
3
+ import { AddOp, LinkOp, RemoveOp, UpdateOp } from "./commit.mjs";
4
+ import { SQLiteDatabase } from "expo-sqlite";
5
+
6
+ //#region src/local/sync.d.ts
7
+ type Op = AddOp | UpdateOp | RemoveOp | LinkOp<View, View>;
8
+ type SyncOp = Op & {
9
+ version: number;
10
+ timestamp: string;
11
+ };
12
+ type SyncPayload = {
13
+ ops: SyncOp[];
14
+ };
15
+ type MergeFn = (local: {
16
+ data: any;
17
+ version: number;
18
+ }, remote: {
19
+ data: any;
20
+ version: number;
21
+ }, viewName: string) => any;
22
+ /**
23
+ * Abstract sync adapter base class.
24
+ */
25
+ declare abstract class SyncAdapter {
26
+ merge?: MergeFn;
27
+ constructor(options?: {
28
+ merge?: MergeFn;
29
+ });
30
+ abstract send(payload: SyncPayload): Promise<void>;
31
+ abstract onReceive(handler: (payload: SyncPayload) => Promise<void>): void;
32
+ abstract fetchInitialState(): Promise<SyncPayload | null>;
33
+ abstract disconnect(): void;
34
+ }
35
+ //#endregion
36
+ export { MergeFn, SyncAdapter, SyncOp, SyncPayload };