@xnetjs/sqlite 0.0.2 → 0.0.3

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.
@@ -16,6 +16,30 @@ interface BrowserSupport {
16
16
  /** Warning message for soft failures (app can still work with fallback) */
17
17
  warning?: string;
18
18
  }
19
+ interface PersistentStorageStatus {
20
+ /** Browser exposes persistence APIs. */
21
+ supported: boolean;
22
+ /** Whether the site is currently persisted. */
23
+ persisted: boolean | null;
24
+ /** Whether the explicit persistence request was granted. */
25
+ granted: boolean | null;
26
+ /** Whether this status was produced by an explicit persistence request. */
27
+ requested: boolean;
28
+ /** Whether the app can present a user action to request persistence. */
29
+ requestable: boolean;
30
+ /** Durable-storage state for UI and diagnostics. */
31
+ state: 'granted' | 'not-granted' | 'unsupported' | 'error';
32
+ /** User-facing explanation of the result. */
33
+ message: string;
34
+ /** Optional usage estimate in bytes. */
35
+ usageBytes?: number;
36
+ /** Optional quota estimate in bytes. */
37
+ quotaBytes?: number;
38
+ }
39
+ interface PersistentStorageRequestOptions {
40
+ /** Request persistent mode instead of only checking current state. */
41
+ request?: boolean;
42
+ }
19
43
  /**
20
44
  * Check if the current browser supports SQLite-WASM with OPFS.
21
45
  *
@@ -46,6 +70,37 @@ interface BrowserSupport {
46
70
  * ```
47
71
  */
48
72
  declare function checkBrowserSupport(): Promise<BrowserSupport>;
73
+ /**
74
+ * Check whether persistent storage is already enabled without prompting or
75
+ * spending a browser heuristic-based permission request during startup.
76
+ */
77
+ declare function checkPersistentStorage(): Promise<PersistentStorageStatus>;
78
+ /**
79
+ * Whether `navigator.storage.persist()` can be called without showing the
80
+ * user a permission prompt.
81
+ *
82
+ * Chromium and WebKit decide the request silently from heuristics (install,
83
+ * notification permission, engagement) and re-evaluate it fresh on every
84
+ * call, so requesting at startup is free. Gecko (desktop Firefox) shows a
85
+ * modal doorhanger, so requests there must stay behind a user gesture.
86
+ * Firefox on iOS (FxiOS) runs WebKit, but we treat anything Firefox-branded
87
+ * as prompt-capable — the worst case is skipping a free request.
88
+ */
89
+ declare function isSilentPersistRequestSafe(): boolean;
90
+ /**
91
+ * Watch the `persistent-storage` permission for changes (e.g. a grant that
92
+ * lands mid-session after the user enables notifications or installs the
93
+ * app). Querying is free — it never spends or triggers a request.
94
+ *
95
+ * Calls `onChange` with the new state whenever the browser reports a
96
+ * change. No-op on browsers without the Permissions API or the
97
+ * `persistent-storage` permission name. Returns an unsubscribe function.
98
+ */
99
+ declare function watchPersistentStoragePermission(onChange: (state: PermissionState) => void): () => void;
100
+ /**
101
+ * Request persistent storage where supported and summarize the result.
102
+ */
103
+ declare function requestPersistentStorage(options?: PersistentStorageRequestOptions): Promise<PersistentStorageStatus>;
49
104
  /**
50
105
  * Show an unsupported browser message to the user.
51
106
  *
@@ -64,4 +119,4 @@ declare function checkBrowserSupport(): Promise<BrowserSupport>;
64
119
  */
65
120
  declare function showUnsupportedBrowserMessage(reason: string): void;
66
121
 
67
- export { type BrowserSupport, checkBrowserSupport, showUnsupportedBrowserMessage };
122
+ export { type BrowserSupport, type PersistentStorageRequestOptions, type PersistentStorageStatus, checkBrowserSupport, checkPersistentStorage, isSilentPersistRequestSafe, requestPersistentStorage, showUnsupportedBrowserMessage, watchPersistentStoragePermission };
@@ -1,8 +1,16 @@
1
1
  import {
2
2
  checkBrowserSupport,
3
- showUnsupportedBrowserMessage
4
- } from "./chunk-ZRR5D2OD.js";
3
+ checkPersistentStorage,
4
+ isSilentPersistRequestSafe,
5
+ requestPersistentStorage,
6
+ showUnsupportedBrowserMessage,
7
+ watchPersistentStoragePermission
8
+ } from "./chunk-2OK46ZBU.js";
5
9
  export {
6
10
  checkBrowserSupport,
7
- showUnsupportedBrowserMessage
11
+ checkPersistentStorage,
12
+ isSilentPersistRequestSafe,
13
+ requestPersistentStorage,
14
+ showUnsupportedBrowserMessage,
15
+ watchPersistentStoragePermission
8
16
  };
@@ -55,6 +55,120 @@ async function checkBrowserSupport() {
55
55
  }
56
56
  return result;
57
57
  }
58
+ async function readStorageEstimate() {
59
+ const estimate = await navigator.storage.estimate?.().catch(() => void 0);
60
+ return {
61
+ usageBytes: estimate?.usage,
62
+ quotaBytes: estimate?.quota
63
+ };
64
+ }
65
+ function getPersistenceMessage(input) {
66
+ const { persisted, requested, granted } = input;
67
+ if (persisted) {
68
+ return "Durable local storage is enabled. This browser agreed to keep your xNet workspace unless you explicitly clear site data.";
69
+ }
70
+ if (!requested) {
71
+ return "Durable storage is not enabled yet. xNet can request stronger local-storage protection when you choose to enable it.";
72
+ }
73
+ if (granted === false) {
74
+ return "This browser declined durable storage for now. xNet keeps working, and browsers re-evaluate the request as install, notification, and usage signals change.";
75
+ }
76
+ return "This browser did not confirm durable storage. xNet will keep working, but local data may be evicted under storage pressure or aggressive cleanup.";
77
+ }
78
+ async function checkPersistentStorage() {
79
+ return requestPersistentStorage({ request: false });
80
+ }
81
+ function isSilentPersistRequestSafe() {
82
+ if (typeof navigator === "undefined") return false;
83
+ return !/Firefox|FxiOS/i.test(navigator.userAgent);
84
+ }
85
+ function watchPersistentStoragePermission(onChange) {
86
+ let active = true;
87
+ let detach = null;
88
+ void (async () => {
89
+ try {
90
+ const status = await navigator.permissions.query({
91
+ name: "persistent-storage"
92
+ });
93
+ if (!active) return;
94
+ const listener = () => onChange(status.state);
95
+ status.addEventListener("change", listener);
96
+ detach = () => status.removeEventListener("change", listener);
97
+ } catch {
98
+ }
99
+ })();
100
+ return () => {
101
+ active = false;
102
+ detach?.();
103
+ detach = null;
104
+ };
105
+ }
106
+ async function requestPersistentStorage(options = { request: true }) {
107
+ const requested = options.request ?? true;
108
+ if (typeof navigator === "undefined" || !navigator.storage) {
109
+ return {
110
+ supported: false,
111
+ persisted: null,
112
+ granted: null,
113
+ requested,
114
+ requestable: false,
115
+ state: "unsupported",
116
+ message: "This browser does not expose storage persistence APIs. Local data can still work, but the browser may evict it under storage pressure."
117
+ };
118
+ }
119
+ const estimate = await readStorageEstimate();
120
+ if (!navigator.storage.persist || !navigator.storage.persisted) {
121
+ return {
122
+ supported: false,
123
+ persisted: null,
124
+ granted: null,
125
+ requested,
126
+ requestable: false,
127
+ state: "unsupported",
128
+ message: "This browser cannot explicitly request durable storage. Local data can still work, but the browser may evict it under storage pressure.",
129
+ ...estimate
130
+ };
131
+ }
132
+ try {
133
+ const alreadyPersisted = await navigator.storage.persisted();
134
+ const granted = alreadyPersisted ? true : requested ? await navigator.storage.persist() : null;
135
+ const persisted = granted ? true : await navigator.storage.persisted();
136
+ if (persisted) {
137
+ return {
138
+ supported: true,
139
+ persisted,
140
+ granted,
141
+ requested,
142
+ requestable: false,
143
+ state: "granted",
144
+ message: getPersistenceMessage({ persisted, requested, granted }),
145
+ ...estimate
146
+ };
147
+ }
148
+ return {
149
+ supported: true,
150
+ persisted,
151
+ granted,
152
+ requested,
153
+ requestable: true,
154
+ state: "not-granted",
155
+ message: getPersistenceMessage({ persisted, requested, granted }),
156
+ ...estimate
157
+ };
158
+ } catch (err) {
159
+ const reason = err instanceof Error ? err.message : String(err);
160
+ return {
161
+ supported: true,
162
+ persisted: null,
163
+ granted: null,
164
+ requested,
165
+ requestable: true,
166
+ state: "error",
167
+ message: `xNet could not confirm durable storage (${reason}). Local data may still work, but persistence guarantees are unclear.`,
168
+ ...estimate
169
+ };
170
+ }
171
+ }
58
172
  function showUnsupportedBrowserMessage(reason) {
59
173
  const container = document.getElementById("app") ?? document.body;
60
174
  container.innerHTML = `
@@ -136,5 +250,9 @@ function escapeHtml(text) {
136
250
 
137
251
  export {
138
252
  checkBrowserSupport,
253
+ checkPersistentStorage,
254
+ isSilentPersistRequestSafe,
255
+ watchPersistentStoragePermission,
256
+ requestPersistentStorage,
139
257
  showUnsupportedBrowserMessage
140
258
  };
@@ -0,0 +1,30 @@
1
+ // src/errors.ts
2
+ var SQLITE_CORRUPT_RESULT_CODE = 11;
3
+ var SQLITE_NOTADB_RESULT_CODE = 26;
4
+ function toErrorLike(error) {
5
+ if (!error || typeof error !== "object") {
6
+ return null;
7
+ }
8
+ return error;
9
+ }
10
+ function isSQLiteCorruptionError(error) {
11
+ return isSQLiteCorruptionErrorInternal(error, /* @__PURE__ */ new Set());
12
+ }
13
+ function isSQLiteCorruptionErrorInternal(error, seen) {
14
+ const current = toErrorLike(error);
15
+ if (!current) {
16
+ return false;
17
+ }
18
+ if (seen.has(error)) {
19
+ return false;
20
+ }
21
+ seen.add(error);
22
+ const message = typeof current.message === "string" ? current.message.toLowerCase() : String(current.message);
23
+ const code = typeof current.code === "string" ? current.code.toUpperCase() : "";
24
+ const resultCode = typeof current.resultCode === "number" ? current.resultCode : null;
25
+ return resultCode === SQLITE_CORRUPT_RESULT_CODE || resultCode === SQLITE_NOTADB_RESULT_CODE || code === "SQLITE_CORRUPT" || code === "SQLITE_NOTADB" || message.includes("sqlite_corrupt") || message.includes("sqlite_notadb") || message.includes("database disk image is malformed") || message.includes("file is not a database") || isSQLiteCorruptionErrorInternal(current.cause, seen);
26
+ }
27
+
28
+ export {
29
+ isSQLiteCorruptionError
30
+ };