@symbiote-native/sqlite 0.1.0

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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +283 -0
  3. package/build/angular/index.d.ts +2 -0
  4. package/build/angular/index.js +2 -0
  5. package/build/angular/sqlite.service.d.ts +41 -0
  6. package/build/angular/sqlite.service.js +127 -0
  7. package/build/core/index.d.ts +10 -0
  8. package/build/core/index.js +6 -0
  9. package/build/core/native-database.d.ts +65 -0
  10. package/build/core/native-database.js +13 -0
  11. package/build/core/native-module.d.ts +25 -0
  12. package/build/core/native-module.js +17 -0
  13. package/build/core/native-session.d.ts +20 -0
  14. package/build/core/native-session.js +1 -0
  15. package/build/core/native-statement.d.ts +45 -0
  16. package/build/core/native-statement.js +6 -0
  17. package/build/core/param-utils.d.ts +28 -0
  18. package/build/core/param-utils.js +126 -0
  19. package/build/core/path-utils.d.ts +6 -0
  20. package/build/core/path-utils.js +29 -0
  21. package/build/core/query-utils.d.ts +17 -0
  22. package/build/core/query-utils.js +37 -0
  23. package/build/core/sqlite-database.d.ts +246 -0
  24. package/build/core/sqlite-database.js +427 -0
  25. package/build/core/sqlite-session.d.ts +84 -0
  26. package/build/core/sqlite-session.js +121 -0
  27. package/build/core/sqlite-statement.d.ts +96 -0
  28. package/build/core/sqlite-statement.js +321 -0
  29. package/build/core/sqlite-tagged-query.d.ts +73 -0
  30. package/build/core/sqlite-tagged-query.js +119 -0
  31. package/build/core/storage.d.ts +107 -0
  32. package/build/core/storage.js +382 -0
  33. package/build/core/types.d.ts +30 -0
  34. package/build/core/types.js +1 -0
  35. package/build/react/index.d.ts +2 -0
  36. package/build/react/index.js +2 -0
  37. package/build/react/sqlite-context.d.ts +25 -0
  38. package/build/react/sqlite-context.js +129 -0
  39. package/build/solid/index.d.ts +2 -0
  40. package/build/solid/index.js +2 -0
  41. package/build/solid/sqlite-context.d.ts +23 -0
  42. package/build/solid/sqlite-context.js +106 -0
  43. package/build/svelte/SQLiteProvider.svelte +88 -0
  44. package/build/svelte/SQLiteProvider.svelte.d.ts +13 -0
  45. package/build/svelte/index.d.ts +3 -0
  46. package/build/svelte/index.js +5 -0
  47. package/build/svelte/sqlite-context.d.ts +11 -0
  48. package/build/svelte/sqlite-context.js +23 -0
  49. package/build/vue/index.d.ts +2 -0
  50. package/build/vue/index.js +2 -0
  51. package/build/vue/sqlite-context.d.ts +21 -0
  52. package/build/vue/sqlite-context.js +69 -0
  53. package/build-ngc/angular/index.d.ts +2 -0
  54. package/build-ngc/angular/index.js +3 -0
  55. package/build-ngc/angular/index.js.map +1 -0
  56. package/build-ngc/angular/sqlite.service.d.ts +44 -0
  57. package/build-ngc/angular/sqlite.service.js +85 -0
  58. package/build-ngc/angular/sqlite.service.js.map +1 -0
  59. package/build-ngc/core/index.d.ts +10 -0
  60. package/build-ngc/core/index.js +7 -0
  61. package/build-ngc/core/index.js.map +1 -0
  62. package/build-ngc/core/native-database.d.ts +65 -0
  63. package/build-ngc/core/native-database.js +14 -0
  64. package/build-ngc/core/native-database.js.map +1 -0
  65. package/build-ngc/core/native-module.d.ts +25 -0
  66. package/build-ngc/core/native-module.js +18 -0
  67. package/build-ngc/core/native-module.js.map +1 -0
  68. package/build-ngc/core/native-session.d.ts +20 -0
  69. package/build-ngc/core/native-session.js +2 -0
  70. package/build-ngc/core/native-session.js.map +1 -0
  71. package/build-ngc/core/native-statement.d.ts +45 -0
  72. package/build-ngc/core/native-statement.js +7 -0
  73. package/build-ngc/core/native-statement.js.map +1 -0
  74. package/build-ngc/core/param-utils.d.ts +28 -0
  75. package/build-ngc/core/param-utils.js +127 -0
  76. package/build-ngc/core/param-utils.js.map +1 -0
  77. package/build-ngc/core/path-utils.d.ts +6 -0
  78. package/build-ngc/core/path-utils.js +30 -0
  79. package/build-ngc/core/path-utils.js.map +1 -0
  80. package/build-ngc/core/query-utils.d.ts +17 -0
  81. package/build-ngc/core/query-utils.js +38 -0
  82. package/build-ngc/core/query-utils.js.map +1 -0
  83. package/build-ngc/core/sqlite-database.d.ts +246 -0
  84. package/build-ngc/core/sqlite-database.js +428 -0
  85. package/build-ngc/core/sqlite-database.js.map +1 -0
  86. package/build-ngc/core/sqlite-session.d.ts +84 -0
  87. package/build-ngc/core/sqlite-session.js +122 -0
  88. package/build-ngc/core/sqlite-session.js.map +1 -0
  89. package/build-ngc/core/sqlite-statement.d.ts +96 -0
  90. package/build-ngc/core/sqlite-statement.js +322 -0
  91. package/build-ngc/core/sqlite-statement.js.map +1 -0
  92. package/build-ngc/core/sqlite-tagged-query.d.ts +73 -0
  93. package/build-ngc/core/sqlite-tagged-query.js +120 -0
  94. package/build-ngc/core/sqlite-tagged-query.js.map +1 -0
  95. package/build-ngc/core/storage.d.ts +107 -0
  96. package/build-ngc/core/storage.js +383 -0
  97. package/build-ngc/core/storage.js.map +1 -0
  98. package/build-ngc/core/types.d.ts +30 -0
  99. package/build-ngc/core/types.js +2 -0
  100. package/build-ngc/core/types.js.map +1 -0
  101. package/native-link.json +12 -0
  102. package/package.json +141 -0
  103. package/src/angular/index.ts +2 -0
  104. package/src/angular/sqlite.service.ts +114 -0
  105. package/src/core/index.ts +40 -0
  106. package/src/core/native-database.ts +118 -0
  107. package/src/core/native-module.ts +57 -0
  108. package/src/core/native-session.ts +64 -0
  109. package/src/core/native-statement.ts +85 -0
  110. package/src/core/param-utils.ts +163 -0
  111. package/src/core/path-utils.ts +38 -0
  112. package/src/core/query-utils.ts +45 -0
  113. package/src/core/sqlite-database.ts +676 -0
  114. package/src/core/sqlite-session.ts +165 -0
  115. package/src/core/sqlite-statement.ts +578 -0
  116. package/src/core/sqlite-tagged-query.ts +160 -0
  117. package/src/core/storage.ts +492 -0
  118. package/src/core/types.ts +33 -0
  119. package/src/react/index.ts +2 -0
  120. package/src/react/sqlite-context.tsx +244 -0
  121. package/src/solid/index.ts +2 -0
  122. package/src/solid/sqlite-context.ts +156 -0
  123. package/src/svelte/SQLiteProvider.svelte +88 -0
  124. package/src/svelte/index.ts +6 -0
  125. package/src/svelte/sqlite-context.ts +32 -0
  126. package/src/svelte/svelte-compile.test-helper.ts +135 -0
  127. package/src/vue/index.ts +2 -0
  128. package/src/vue/sqlite-context.ts +105 -0
@@ -0,0 +1,244 @@
1
+ // React <SQLiteProvider> + useSQLiteContext(), ported from expo-sqlite's hooks.tsx (sdk-57,
2
+ // .vendors/expo) onto this package's own openDatabaseAsync/SQLiteDatabase. Deliberately excludes
3
+ // `assetSource` (bundling a database from a require()'d asset needs expo-asset, out of scope for
4
+ // this pass — see the README). React 19's built-in `use()` replaces upstream's private
5
+ // Suspense-promise polyfill (~40 LOC of `ReactUsePromise` plumbing, unnecessary now that the
6
+ // peer range is react >=19.0.0). Unlike upstream, `onInit` is NOT invoked here — this package's
7
+ // own `openDatabaseAsync` already runs it internally before resolving (see
8
+ // `IOnInitCallback`'s doc comment in `core/types.ts`), so the provider only has to forward it.
9
+
10
+ import {
11
+ createContext,
12
+ memo,
13
+ use,
14
+ useContext,
15
+ useEffect,
16
+ useRef,
17
+ useState,
18
+ type ReactElement,
19
+ type ReactNode,
20
+ } from 'react';
21
+ import {
22
+ openDatabaseAsync,
23
+ type IOnInitCallback,
24
+ type ISQLiteOpenOptions,
25
+ type SQLiteDatabase,
26
+ } from '../core';
27
+
28
+ export type ISQLiteProviderProps = {
29
+ /** The name of the database file to open. */
30
+ databaseName: string;
31
+ /** The directory where the database file is located. @default defaultDatabaseDirectory */
32
+ directory?: string;
33
+ /** Open options. */
34
+ options?: ISQLiteOpenOptions;
35
+ /** The children to render. */
36
+ children: ReactNode;
37
+ /** Run once, right after the native handle opens and before children render. */
38
+ onInit?: IOnInitCallback;
39
+ /** Handle errors from SQLiteProvider. @default rethrow the error */
40
+ onError?: (error: Error) => void;
41
+ /** Enable React.Suspense integration. @default false */
42
+ useSuspense?: boolean;
43
+ };
44
+
45
+ const SQLiteContext = createContext<SQLiteDatabase | null>(null);
46
+
47
+ function isRecord(value: unknown): value is Record<string, unknown> {
48
+ return typeof value === 'object' && value !== null;
49
+ }
50
+
51
+ /** Structural equality, recursing into plain objects — used only to keep the memo comparator
52
+ * below from reopening a database when a parent re-passes a deep-equal but new `options` object. */
53
+ function deepEqual(a: unknown, b: unknown): boolean {
54
+ if (a === b) return true;
55
+ if (!isRecord(a) || !isRecord(b)) return false;
56
+ const aKeys = Object.keys(a);
57
+ const bKeys = Object.keys(b);
58
+ return (
59
+ aKeys.length === bKeys.length &&
60
+ aKeys.every(key => deepEqual(a[key], b[key]))
61
+ );
62
+ }
63
+
64
+ function propsAreEqual(
65
+ prev: ISQLiteProviderProps,
66
+ next: ISQLiteProviderProps,
67
+ ): boolean {
68
+ return (
69
+ prev.databaseName === next.databaseName &&
70
+ deepEqual(prev.options, next.options) &&
71
+ prev.directory === next.directory &&
72
+ prev.onInit === next.onInit &&
73
+ prev.onError === next.onError &&
74
+ prev.useSuspense === next.useSuspense
75
+ );
76
+ }
77
+
78
+ function toError(caught: unknown): Error {
79
+ return caught instanceof Error ? caught : new Error(String(caught));
80
+ }
81
+
82
+ /**
83
+ * Context.Provider component that provides a SQLite database to all children. All descendants
84
+ * of this component can access the database via `useSQLiteContext`.
85
+ */
86
+ export const SQLiteProvider = memo(function SQLiteProvider({
87
+ children,
88
+ onError,
89
+ useSuspense = false,
90
+ ...props
91
+ }: ISQLiteProviderProps): ReactElement | null {
92
+ if (onError != null && useSuspense) {
93
+ throw new Error(
94
+ 'Cannot use `onError` with `useSuspense`, use error boundaries instead.',
95
+ );
96
+ }
97
+
98
+ if (useSuspense) {
99
+ return (
100
+ <SQLiteProviderSuspense {...props}>{children}</SQLiteProviderSuspense>
101
+ );
102
+ }
103
+
104
+ return (
105
+ <SQLiteProviderNonSuspense {...props} onError={onError}>
106
+ {children}
107
+ </SQLiteProviderNonSuspense>
108
+ );
109
+ }, propsAreEqual);
110
+
111
+ /** A hook for accessing the SQLite database provided by an ancestor `<SQLiteProvider>`. */
112
+ export function useSQLiteContext(): SQLiteDatabase {
113
+ const context = useContext(SQLiteContext);
114
+ if (context == null) {
115
+ throw new Error('useSQLiteContext must be used within a <SQLiteProvider>');
116
+ }
117
+ return context;
118
+ }
119
+
120
+ //#region Internals
121
+
122
+ function SQLiteProviderNonSuspense({
123
+ databaseName,
124
+ directory,
125
+ options,
126
+ children,
127
+ onInit,
128
+ onError,
129
+ }: Omit<ISQLiteProviderProps, 'useSuspense'>): ReactElement | null {
130
+ const databaseRef = useRef<SQLiteDatabase | null>(null);
131
+ const [loading, setLoading] = useState(true);
132
+ const [error, setError] = useState<Error | null>(null);
133
+
134
+ useEffect(() => {
135
+ async function setup(): Promise<void> {
136
+ try {
137
+ const db = await openDatabaseAsync(
138
+ databaseName,
139
+ { ...options, onInit },
140
+ directory,
141
+ );
142
+ databaseRef.current = db;
143
+ setLoading(false);
144
+ } catch (caught) {
145
+ setError(toError(caught));
146
+ }
147
+ }
148
+
149
+ async function teardown(db: SQLiteDatabase | null): Promise<void> {
150
+ try {
151
+ await db?.closeAsync();
152
+ } catch (caught) {
153
+ setError(toError(caught));
154
+ }
155
+ }
156
+
157
+ setup();
158
+
159
+ return () => {
160
+ const db = databaseRef.current;
161
+ teardown(db);
162
+ databaseRef.current = null;
163
+ setLoading(true);
164
+ };
165
+ }, [databaseName, directory, options, onInit]);
166
+
167
+ if (error != null) {
168
+ const handler =
169
+ onError ??
170
+ ((caught: Error) => {
171
+ throw caught;
172
+ });
173
+ handler(error);
174
+ }
175
+ if (loading || databaseRef.current == null) {
176
+ return null;
177
+ }
178
+ return (
179
+ <SQLiteContext.Provider value={databaseRef.current}>
180
+ {children}
181
+ </SQLiteContext.Provider>
182
+ );
183
+ }
184
+
185
+ type IDatabaseCacheEntry = {
186
+ databaseName: string;
187
+ directory: string | undefined;
188
+ options: ISQLiteOpenOptions | undefined;
189
+ onInit: IOnInitCallback | undefined;
190
+ promise: Promise<SQLiteDatabase>;
191
+ };
192
+
193
+ // A single module-scope cache entry, mirroring upstream: only ONE suspended database is tracked
194
+ // across the whole app at a time, and a second concurrent <SQLiteProvider useSuspense> for a
195
+ // different database closes the first before opening the next. Faithful to upstream's own
196
+ // `databaseInstance` shape, not a per-provider cache.
197
+ let databaseCache: IDatabaseCacheEntry | null = null;
198
+
199
+ function getDatabaseAsync(
200
+ databaseName: string,
201
+ directory: string | undefined,
202
+ options: ISQLiteOpenOptions | undefined,
203
+ onInit: IOnInitCallback | undefined,
204
+ ): Promise<SQLiteDatabase> {
205
+ if (
206
+ databaseCache != null &&
207
+ databaseCache.databaseName === databaseName &&
208
+ databaseCache.directory === directory &&
209
+ databaseCache.options === options &&
210
+ databaseCache.onInit === onInit
211
+ ) {
212
+ return databaseCache.promise;
213
+ }
214
+
215
+ const previous = databaseCache;
216
+ const promise =
217
+ previous != null
218
+ ? previous.promise
219
+ .then(db => db.closeAsync())
220
+ .then(() =>
221
+ openDatabaseAsync(databaseName, { ...options, onInit }, directory),
222
+ )
223
+ : openDatabaseAsync(databaseName, { ...options, onInit }, directory);
224
+
225
+ databaseCache = { databaseName, directory, options, onInit, promise };
226
+ return promise;
227
+ }
228
+
229
+ function SQLiteProviderSuspense({
230
+ databaseName,
231
+ directory,
232
+ options,
233
+ children,
234
+ onInit,
235
+ }: Omit<ISQLiteProviderProps, 'onError' | 'useSuspense'>): ReactElement {
236
+ const database = use(
237
+ getDatabaseAsync(databaseName, directory, options, onInit),
238
+ );
239
+ return (
240
+ <SQLiteContext.Provider value={database}>{children}</SQLiteContext.Provider>
241
+ );
242
+ }
243
+
244
+ //#endregion
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export * from './sqlite-context';
@@ -0,0 +1,156 @@
1
+ // Solid <SQLiteProvider> + useSQLiteContext(), ported from expo-sqlite's hooks.tsx (sdk-57,
2
+ // .vendors/expo) onto this package's own openDatabaseAsync/SQLiteDatabase — see ../react's
3
+ // sqlite-context.tsx for the React twin this mirrors the OBSERVABLE contract of (the React-only
4
+ // mechanics, useEffect/use()/Suspense, don't transfer).
5
+ //
6
+ // Plain `.ts`, no JSX: this package's tsconfig has no per-directory override and inherits the
7
+ // root `jsx: "react-jsx"` (set for the React entry), so a `.tsx` file here would type-check its
8
+ // JSX against React's JSX namespace, not Solid's — the same trap `solid-jsx-namespace.md`
9
+ // documents for the adapter itself. `createComponent` is called directly instead, exactly as
10
+ // `packages/navigation/src/solid/stack/index.ts` does throughout.
11
+ //
12
+ // createResource is the right tool: it natively models pending/error/ready, and — the load-bearing
13
+ // part — reading an ERRORED resource accessor THROWS SYNCHRONOUSLY on every read (solid-js's own
14
+ // `read()`: `if (err !== undefined && !pr) throw err`), tracked or not. That is Solid's real
15
+ // equivalent of upstream's `handler = (e) => { throw e }` default: no onError -> the throw surfaces
16
+ // wherever `db()` is read (here, inside <Show>'s own tracking), and propagates through Solid's
17
+ // error-context chain exactly like any other component/memo/effect throw in this adapter
18
+ // (adapters/solid/src/render.ts's `catchError` + rethrow at the mount root — the tested,
19
+ // established "unhandled async failure" convention for this Solid adapter). With onError present,
20
+ // the fetcher calls it and resolves to `undefined` instead of throwing, so <Show> simply renders
21
+ // nothing rather than propagating — matching upstream's "loading || !db -> render null" forever.
22
+ //
23
+ // Closing the previous database before opening the next reuses createResource's own
24
+ // ResourceFetcherInfo.value (the last successfully resolved value) rather than a second piece of
25
+ // state — it IS "the database this resource had opened before". `openedDb` is a plain closure
26
+ // variable (not read through the resource accessor) so `onCleanup` never has to read a possibly
27
+ // -errored resource, which would itself throw during teardown.
28
+ //
29
+ // assetSource is deliberately NOT ported: it needs expo-asset, out of scope (see the README).
30
+ // useSuspense is deliberately NOT offered: Solid's own <Suspense> already composes with a plain
31
+ // createResource-backed component with zero extra code (any ancestor <Suspense> defers on this
32
+ // resource for free), so there is no second, opt-in code path to build the way React needs one —
33
+ // <prop_types_split_agnostic_vs_per_adapter>'s "framework-idiom divergence" class of omission.
34
+
35
+ import {
36
+ createComponent,
37
+ createContext,
38
+ createResource,
39
+ onCleanup,
40
+ useContext,
41
+ type Accessor,
42
+ type JSX,
43
+ } from 'solid-js';
44
+ import { Show } from 'solid-js';
45
+ import {
46
+ openDatabaseAsync,
47
+ type IOnInitCallback,
48
+ type ISQLiteOpenOptions,
49
+ type SQLiteDatabase,
50
+ } from '../core';
51
+
52
+ export type ISQLiteProviderProps = {
53
+ /** The name of the database file to open. */
54
+ databaseName: string;
55
+ /** The directory where the database file is located. @default defaultDatabaseDirectory */
56
+ directory?: string;
57
+ /** Open options. */
58
+ options?: ISQLiteOpenOptions;
59
+ /** The children to render. */
60
+ children: JSX.Element;
61
+ /** Run once, right after the native handle opens and before children render. */
62
+ onInit?: IOnInitCallback;
63
+ /** Handle errors from SQLiteProvider. @default rethrow (propagate through Solid's error context) */
64
+ onError?: (error: Error) => void;
65
+ };
66
+
67
+ const SQLiteContext = createContext<SQLiteDatabase>();
68
+
69
+ function toError(cause: unknown): Error {
70
+ return cause instanceof Error ? cause : new Error(String(cause));
71
+ }
72
+
73
+ type IDbSource = {
74
+ databaseName: string;
75
+ directory: string | undefined;
76
+ options: ISQLiteOpenOptions | undefined;
77
+ onInit: IOnInitCallback | undefined;
78
+ };
79
+
80
+ /**
81
+ * Context.Provider component that provides a SQLite database to all children. All descendants
82
+ * of this component can access the database via `useSQLiteContext`.
83
+ */
84
+ export function SQLiteProvider(props: ISQLiteProviderProps): JSX.Element {
85
+ // The last database THIS provider successfully opened — read only by onCleanup, never through
86
+ // the resource accessor (which throws when the resource is in its errored state).
87
+ let openedDb: SQLiteDatabase | undefined;
88
+
89
+ const [db] = createResource<SQLiteDatabase | undefined, IDbSource>(
90
+ () => ({
91
+ databaseName: props.databaseName,
92
+ directory: props.directory,
93
+ options: props.options,
94
+ onInit: props.onInit,
95
+ }),
96
+ async (source, info) => {
97
+ if (info.value !== undefined) {
98
+ await info.value.closeAsync();
99
+ }
100
+ try {
101
+ const opened = await openDatabaseAsync(
102
+ source.databaseName,
103
+ { ...source.options, onInit: source.onInit },
104
+ source.directory,
105
+ );
106
+ openedDb = opened;
107
+ return opened;
108
+ } catch (cause) {
109
+ openedDb = undefined;
110
+ const error = toError(cause);
111
+ if (props.onError !== undefined) {
112
+ props.onError(error);
113
+ return undefined;
114
+ }
115
+ throw error;
116
+ }
117
+ },
118
+ );
119
+
120
+ onCleanup(() => {
121
+ void openedDb?.closeAsync();
122
+ });
123
+
124
+ function renderReady(database: Accessor<SQLiteDatabase>): JSX.Element {
125
+ return createComponent(SQLiteContext.Provider, {
126
+ get value(): SQLiteDatabase {
127
+ return database();
128
+ },
129
+ get children(): JSX.Element {
130
+ return props.children;
131
+ },
132
+ });
133
+ }
134
+
135
+ // A direct call, not createComponent(Show, …): Show is overloaded (keyed vs non-keyed), and
136
+ // TypeScript resolves an overloaded value against createComponent's generic Component<T> by its
137
+ // LAST signature only — reporting the non-keyed props object as missing `keyed: true`. Calling
138
+ // it directly goes through normal overload resolution instead, which picks correctly. Show's own
139
+ // body needs no extra untrack wrapping (it already wraps every read in its own createMemo), so
140
+ // nothing behavioral is lost by skipping createComponent here.
141
+ return Show({
142
+ get when(): SQLiteDatabase | undefined {
143
+ return db();
144
+ },
145
+ children: renderReady,
146
+ });
147
+ }
148
+
149
+ /** A primitive for accessing the SQLite database provided by an ancestor `<SQLiteProvider>`. */
150
+ export function useSQLiteContext(): SQLiteDatabase {
151
+ const context = useContext(SQLiteContext);
152
+ if (context === undefined) {
153
+ throw new Error('useSQLiteContext must be used within a <SQLiteProvider>');
154
+ }
155
+ return context;
156
+ }
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ // SQLiteProvider: the Svelte equivalent of upstream expo-sqlite's SQLiteProvider/
3
+ // useSQLiteContext pair (.vendors/expo packages/expo-sqlite/src/hooks.tsx, sdk-57). Opens the
4
+ // database in an `$effect` (re-running whenever databaseName/directory/options/onInit change —
5
+ // $effect's own cleanup-then-rerun IS upstream's teardown()-then-setup() dance), renders
6
+ // nothing until it resolves, and hands the resolved `SQLiteDatabase` to descendants via
7
+ // `useSQLiteContext` (./sqlite-context).
8
+ //
9
+ // Context is provided through a BOXED GETTER set synchronously during init, mirroring
10
+ // navigation-context.ts: setContext only works at component-init time, but the database
11
+ // resolves later, so the getter is handed out immediately and its underlying `$state` is
12
+ // written once the effect settles.
13
+ //
14
+ // Deliberately excludes `assetSource` (needs `expo-asset`, out of scope for this package — see
15
+ // the core layer's own IOnInitCallback doc comment) and `useSuspense` (Svelte has no built-in
16
+ // Suspense primitive).
17
+ //
18
+ // Unlike upstream, `onInit` is not called from here — this package's own `openDatabaseAsync`
19
+ // already runs it internally before resolving (see `IOnInitCallback`'s doc comment in
20
+ // `core/types.ts`), so this component only forwards it through `options`.
21
+ import type { Snippet } from 'svelte';
22
+ import { openDatabaseAsync } from '../core';
23
+ import type { IOnInitCallback, ISQLiteOpenOptions, SQLiteDatabase } from '../core';
24
+ import { setSQLiteContext } from './sqlite-context';
25
+
26
+ let {
27
+ databaseName,
28
+ directory,
29
+ options,
30
+ onInit,
31
+ onError,
32
+ children,
33
+ }: {
34
+ databaseName: string;
35
+ directory?: string;
36
+ options?: ISQLiteOpenOptions;
37
+ onInit?: IOnInitCallback;
38
+ onError?: (error: Error) => void;
39
+ children?: Snippet;
40
+ } = $props();
41
+
42
+ let db = $state<SQLiteDatabase | undefined>(undefined);
43
+
44
+ setSQLiteContext({
45
+ get current(): SQLiteDatabase | undefined {
46
+ return db;
47
+ },
48
+ });
49
+
50
+ $effect(() => {
51
+ const name = databaseName;
52
+ const dir = directory;
53
+ const openOptions = options;
54
+ const init = onInit;
55
+
56
+ let cancelled = false;
57
+ let opened: SQLiteDatabase | undefined;
58
+
59
+ openDatabaseAsync(name, { ...openOptions, onInit: init }, dir)
60
+ .then(database => {
61
+ if (cancelled) {
62
+ void database.closeAsync();
63
+ return;
64
+ }
65
+ opened = database;
66
+ db = database;
67
+ })
68
+ .catch((error: unknown) => {
69
+ if (cancelled) return;
70
+ const err = error instanceof Error ? error : new Error(String(error));
71
+ // Upstream default: rethrow. Svelte has no error-boundary equivalent to catch a
72
+ // synchronous throw from here, so "rethrow" surfaces as an unhandled rejection — the
73
+ // same observable failure (an uncaught error) upstream produces without `onError`.
74
+ if (onError) onError(err);
75
+ else throw err;
76
+ });
77
+
78
+ return () => {
79
+ cancelled = true;
80
+ db = undefined;
81
+ if (opened) void opened.closeAsync();
82
+ };
83
+ });
84
+ </script>
85
+
86
+ {#if db}
87
+ {@render children?.()}
88
+ {/if}
@@ -0,0 +1,6 @@
1
+ // @symbiote-native/sqlite/svelte: SQLiteProvider + useSQLiteContext over the framework-agnostic
2
+ // core. Mirrors packages/navigation/src/svelte/index.ts's barrel shape for a `.svelte` component.
3
+
4
+ export * from '../core';
5
+ export { default as SQLiteProvider } from './SQLiteProvider.svelte';
6
+ export * from './sqlite-context';
@@ -0,0 +1,32 @@
1
+ // Svelte context accessor for the database `<SQLiteProvider>` opens. Mirrors
2
+ // packages/navigation/src/svelte/navigation-context.ts's boxed-getter shape: Svelte's
3
+ // setContext only runs synchronously during component init, while the database resolves later
4
+ // (an async openDatabaseAsync), so the provider hands out a getter object up front and mutates
5
+ // the value it reads later instead of calling setContext once resolution finishes.
6
+
7
+ import { getContext, setContext } from 'svelte';
8
+ import type { SQLiteDatabase } from '../core';
9
+
10
+ export type ISQLiteContextValue = {
11
+ readonly current: SQLiteDatabase | undefined;
12
+ };
13
+
14
+ const SQLITE_CONTEXT_KEY = Symbol('symbiote-sqlite-context');
15
+
16
+ // Written only from <SQLiteProvider>'s own init, never by app code.
17
+ export function setSQLiteContext(value: ISQLiteContextValue): void {
18
+ setContext(SQLITE_CONTEXT_KEY, value);
19
+ }
20
+
21
+ /**
22
+ * A global function for accessing the SQLite database provided by the nearest ancestor
23
+ * `<SQLiteProvider>`. Throws if called outside one — matching upstream expo-sqlite's
24
+ * `useSQLiteContext` (`.vendors/expo` packages/expo-sqlite/src/hooks.tsx, sdk-57).
25
+ */
26
+ export function useSQLiteContext(): SQLiteDatabase {
27
+ const value = getContext<ISQLiteContextValue | undefined>(SQLITE_CONTEXT_KEY);
28
+ if (value === undefined || value.current === undefined) {
29
+ throw new Error('useSQLiteContext must be used within a <SQLiteProvider>');
30
+ }
31
+ return value.current;
32
+ }
@@ -0,0 +1,135 @@
1
+ // The compile-and-load harness `SQLiteProvider.smoke.test.ts` uses. Copied verbatim (mechanism,
2
+ // not content — nothing here is navigation-specific) from
3
+ // packages/navigation/src/svelte/svelte-compile.test-helper.ts, which is not part of that
4
+ // package's public exports (a co-located test helper), so it can't be imported across the
5
+ // package boundary.
6
+ //
7
+ // There is no `.svelte`-aware bundler wired into this repo's vitest (svelte-adapter-dom-shim
8
+ // skill §15, a deliberate choice), so a test that needs to RUN a component compiles the real
9
+ // source with `svelte/compiler` itself and dynamic-`import()`s the output. Three rules fall out
10
+ // of that, all of them learned the hard way in the navigation adapter's own smokes:
11
+ //
12
+ // 1. The compiled file must sit NEXT TO the real source, because its own relative imports
13
+ // resolve from wherever the compiled FILE lives, not from where the source did.
14
+ // 2. Every `.svelte` specifier inside the compiled output has to be rewritten to the compiled
15
+ // twin, recursively.
16
+ // 3. Node's `import()` caches by path, so re-writing new content to a path a previous test
17
+ // already imported hands back the STALE module. Every harness instance therefore stamps its
18
+ // own id into the filenames.
19
+
20
+ import { compile } from 'svelte/compiler';
21
+ import { basename, dirname, join, relative, resolve } from 'node:path';
22
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
23
+ import { pathToFileURL } from 'node:url';
24
+ // The real Metro pipeline's own `.svelte.ts` compile step (TS-strip, then `compileModule` to
25
+ // desugar the runes). Reused rather than reimplemented. Default-imported because it is a `.cjs`
26
+ // module.
27
+ import metroSvelteTransformer from '@symbiote-native/svelte/metro-svelte-transformer';
28
+
29
+ const {
30
+ compileSvelteModuleFile,
31
+ }: {
32
+ compileSvelteModuleFile: (source: string, filename: string) => string;
33
+ } = metroSvelteTransformer;
34
+
35
+ const COMPILE_OPTIONS = {
36
+ generate: 'client',
37
+ fragments: 'tree',
38
+ css: 'external',
39
+ } as const;
40
+ const SVELTE_SPECIFIER_PATTERN = /(['"])(\.{1,2}\/[^'"]*\.svelte)\1/g;
41
+
42
+ export type ISvelteHarness = {
43
+ // Compile a real `.svelte` file plus everything it imports; returns the compiled entry path.
44
+ compileFile(sveltePath: string): string;
45
+ // Compile an inline fixture written into `dir`, so its own relative imports resolve from there.
46
+ compileSource(dir: string, name: string, source: string): string;
47
+ cleanup(): void;
48
+ };
49
+
50
+ export function createSvelteHarness(
51
+ id: string,
52
+ aliases: Readonly<Record<string, string>> = {},
53
+ ): ISvelteHarness {
54
+ const written: string[] = [];
55
+ const compiledBySource = new Map<string, string>();
56
+
57
+ function outPathFor(sveltePath: string): string {
58
+ const stem = basename(sveltePath).replace(/\./g, '-');
59
+ return join(dirname(sveltePath), `.smoke-compiled-${id}-${stem}.mjs`);
60
+ }
61
+
62
+ function relativeSpecifier(fromFile: string, target: string): string {
63
+ const specifier = relative(dirname(fromFile), target);
64
+ return specifier.startsWith('..') ? specifier : `./${specifier}`;
65
+ }
66
+
67
+ function rewrite(code: string, outPath: string): string {
68
+ let rewritten = code.replace(
69
+ SVELTE_SPECIFIER_PATTERN,
70
+ (_match, quote: string, spec: string) => {
71
+ const childSource = resolve(dirname(outPath), spec);
72
+ const childOut = compileFile(childSource);
73
+ return `${quote}${relativeSpecifier(outPath, childOut)}${quote}`;
74
+ },
75
+ );
76
+ for (const [specifier, target] of Object.entries(aliases)) {
77
+ rewritten = rewritten
78
+ .split(`'${specifier}'`)
79
+ .join(`'${relativeSpecifier(outPath, target)}'`);
80
+ rewritten = rewritten
81
+ .split(`"${specifier}"`)
82
+ .join(`"${relativeSpecifier(outPath, target)}"`);
83
+ }
84
+ return rewritten;
85
+ }
86
+
87
+ function emit(
88
+ sveltePath: string,
89
+ source: string,
90
+ isRuneModule: boolean,
91
+ ): string {
92
+ const outPath = outPathFor(sveltePath);
93
+ // Registered BEFORE compiling so a cycle between two components terminates.
94
+ compiledBySource.set(sveltePath, outPath);
95
+ written.push(outPath);
96
+ const filename = basename(sveltePath);
97
+ const code = isRuneModule
98
+ ? compileSvelteModuleFile(source, `${filename}.ts`)
99
+ : compile(source, { ...COMPILE_OPTIONS, filename }).js.code;
100
+ writeFileSync(outPath, rewrite(code, outPath));
101
+ return outPath;
102
+ }
103
+
104
+ function compileFile(sveltePath: string): string {
105
+ const cached = compiledBySource.get(sveltePath);
106
+ if (cached !== undefined) return cached;
107
+ const runeModulePath = `${sveltePath}.ts`;
108
+ if (!existsSync(sveltePath) && existsSync(runeModulePath)) {
109
+ return emit(sveltePath, readFileSync(runeModulePath, 'utf8'), true);
110
+ }
111
+ return emit(sveltePath, readFileSync(sveltePath, 'utf8'), false);
112
+ }
113
+
114
+ return {
115
+ compileFile,
116
+ compileSource(dir, name, source) {
117
+ return emit(join(dir, `${name}.svelte`), source, false);
118
+ },
119
+ cleanup() {
120
+ for (const file of written) rmSync(file, { force: true });
121
+ written.length = 0;
122
+ compiledBySource.clear();
123
+ },
124
+ };
125
+ }
126
+
127
+ // The compiled module's default export, narrowed without a cast so a test never silently mounts
128
+ // `undefined`.
129
+ export async function loadComponent(compiledPath: string): Promise<unknown> {
130
+ const mod: unknown = await import(pathToFileURL(compiledPath).href);
131
+ if (mod === null || typeof mod !== 'object' || !('default' in mod)) {
132
+ throw new Error(`${compiledPath} produced no default export`);
133
+ }
134
+ return mod.default;
135
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export * from './sqlite-context';