Skip to content

Commit aee7933

Browse files
committed
Add expo driver
1 parent abdde14 commit aee7933

16 files changed

Lines changed: 5629 additions & 8 deletions

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
node_modules/
2-
test/demo/dist/
2+
test/demo-vite/dist/
3+
test/demo-metro/.expo/
4+
test/demo-metro/dist/
5+
test/demo-metro/node_modules/

expo/index.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,61 @@
1-
export function expoDriver() {}
1+
import { addDatabaseChangeListener, openDatabaseAsync } from 'expo-sqlite'
2+
3+
export function expoDriver(filename) {
4+
let dbReady = openDatabaseAsync(filename, { enableChangeListener: true })
5+
let subscribers = new Map()
6+
let nextId = 0
7+
8+
async function notifySubscribers() {
9+
let db = await dbReady
10+
await Promise.all(
11+
Array.from(subscribers.values()).map(async sub => {
12+
let rows = await db.getAllAsync(sub.query, sub.params)
13+
sub.cb(rows)
14+
})
15+
)
16+
}
17+
18+
let subscription = addDatabaseChangeListener(() => {
19+
void notifySubscribers()
20+
})
21+
22+
let driver = {
23+
subscribe(query, params, cb) {
24+
let id = nextId++
25+
subscribers.set(id, { query, params, cb })
26+
void dbReady.then(db => {
27+
void db.getAllAsync(query, params).then(rows => {
28+
if (subscribers.has(id)) cb(rows)
29+
})
30+
})
31+
return () => {
32+
subscribers.delete(id)
33+
}
34+
},
35+
36+
async exec(query, params) {
37+
let db = await dbReady
38+
await db.runAsync(query, params)
39+
},
40+
41+
async transaction(callback) {
42+
let db = await dbReady
43+
await db.withTransactionAsync(async () => {
44+
await callback({
45+
subscribe: driver.subscribe,
46+
async exec(query, params) {
47+
await db.runAsync(query, params)
48+
}
49+
})
50+
})
51+
},
52+
53+
async close() {
54+
subscription.remove()
55+
subscribers.clear()
56+
let db = await dbReady
57+
await db.closeAsync()
58+
}
59+
}
60+
return driver
61+
}

oxlint.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default defineConfig({
1515
}
1616
},
1717
{
18-
files: ['test/demo/*.ts'],
18+
files: ['test/demo-vite/*.ts'],
1919
rules: {
2020
'no-console': 'off',
2121
'typescript/no-unsafe-assignment': 'off',

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@
3737
"test:coverage": "pnpm bnt --coverage 100 --coverage-exclude 'test/*'",
3838
"test:types": "check-dts",
3939
"test:size": "size-limit",
40-
"test:build": "vite build ./test/demo",
40+
"test:build:rest": "vite build ./test/demo-vite",
41+
"test:build:metro": "cd test/demo-metro && pnpm expo export --platform web",
4142
"test": "pnpm run /^test:/",
42-
"start": "vite ./test/demo --host 0.0.0.0"
43+
"start:vite": "vite ./test/demo-vite --host 0.0.0.0",
44+
"start:metro": "cd test/demo-metro && pnpm expo start --web --port 5174",
45+
"start": "pnpm run /^start:/"
4346
},
4447
"devDependencies": {
4548
"@electric-sql/pglite": "^0.4.3",

test/demo-metro/.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node-linker=hoisted

test/demo-metro/App.jsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { useStore } from '@nanostores/react'
2+
import { Button, StyleSheet, Text, View } from 'react-native'
3+
4+
import { expoDriver } from '../../expo/index.js'
5+
import { migrateIfNeeded, openDb } from '../../index.js'
6+
7+
let db = openDb(expoDriver('expo-demo.sqlite'))
8+
9+
let $migration = migrateIfNeeded(db, 1, async prevVersion => {
10+
if (prevVersion < 1) {
11+
await db.exec`CREATE TABLE counters
12+
(id INTEGER PRIMARY KEY AUTOINCREMENT, value INTEGER NOT NULL DEFAULT 0)`
13+
}
14+
})
15+
16+
function CounterList() {
17+
let $counters = db.store`SELECT * FROM counters ORDER BY id`
18+
let state = useStore($counters)
19+
20+
if (state.isLoading) {
21+
return <Text>Loading…</Text>
22+
}
23+
24+
let counters = state.value ?? []
25+
26+
return (
27+
<View>
28+
{counters.map(({ id, value }) => (
29+
<View key={id} style={styles.counter}>
30+
<Text style={styles.id}>#{id}</Text>
31+
<Button
32+
title="−"
33+
onPress={() =>
34+
db.exec`UPDATE counters SET value = value - 1 WHERE id = ${id}`
35+
}
36+
/>
37+
<Text style={styles.value}>{value}</Text>
38+
<Button
39+
title="+"
40+
onPress={() =>
41+
db.exec`UPDATE counters SET value = value + 1 WHERE id = ${id}`
42+
}
43+
/>
44+
<Button
45+
title="Delete"
46+
onPress={() => db.exec`DELETE FROM counters WHERE id = ${id}`}
47+
/>
48+
</View>
49+
))}
50+
<Button
51+
title="Add counter"
52+
onPress={() => db.exec`INSERT INTO counters (value) VALUES (0)`}
53+
/>
54+
</View>
55+
)
56+
}
57+
58+
function renderContent(migration) {
59+
if ('applying' in migration) return <Text>Running migrations…</Text>
60+
if ('outdated' in migration) return <Text>Page outdated, please reload.</Text>
61+
return <CounterList />
62+
}
63+
64+
export default function App() {
65+
let migration = useStore($migration)
66+
67+
return (
68+
<View style={styles.container}>
69+
<Text style={styles.title}>Nano Stores SQL Demo (Expo)</Text>
70+
{renderContent(migration)}
71+
</View>
72+
)
73+
}
74+
75+
let styles = StyleSheet.create({
76+
container: {
77+
flex: 1,
78+
padding: 40,
79+
maxWidth: 480
80+
},
81+
title: {
82+
fontSize: 24,
83+
fontWeight: 'bold',
84+
marginBottom: 20
85+
},
86+
counter: {
87+
flexDirection: 'row',
88+
alignItems: 'center',
89+
gap: 8,
90+
marginVertical: 4
91+
},
92+
id: {
93+
minWidth: 40
94+
},
95+
value: {
96+
minWidth: 40,
97+
textAlign: 'center'
98+
}
99+
})

test/demo-metro/app.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"expo": {
3+
"name": "Nano Stores SQL Demo",
4+
"slug": "demo-rn",
5+
"version": "0.0.0",
6+
"platforms": ["web"],
7+
"web": {
8+
"bundler": "metro"
9+
}
10+
}
11+
}

test/demo-metro/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { registerRootComponent } from 'expo'
2+
import App from './App'
3+
4+
registerRootComponent(App)

test/demo-metro/metro.config.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
let { getDefaultConfig } = require('expo/metro-config')
2+
let path = require('path')
3+
4+
let projectRoot = __dirname
5+
let monorepoRoot = path.resolve(projectRoot, '../..')
6+
7+
let config = getDefaultConfig(projectRoot)
8+
9+
config.watchFolders = [monorepoRoot]
10+
config.resolver.nodeModulesPaths = [
11+
path.resolve(projectRoot, 'node_modules'),
12+
path.resolve(monorepoRoot, 'node_modules')
13+
]
14+
config.resolver.disableHierarchicalLookup = true
15+
16+
// Add wasm asset support
17+
config.resolver.assetExts.push('wasm')
18+
19+
// Add COEP and COOP headers to support SharedArrayBuffer
20+
config.server.enhanceMiddleware = middleware => {
21+
return (req, res, next) => {
22+
res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless')
23+
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
24+
middleware(req, res, next)
25+
}
26+
}
27+
28+
module.exports = config

test/demo-metro/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "demo-rn",
3+
"version": "0.0.0",
4+
"private": true,
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "expo start --web"
8+
},
9+
"dependencies": {
10+
"@nanostores/react": "^1.1.0",
11+
"expo": "~55.0.11",
12+
"expo-sqlite": "~55.0.13",
13+
"nanostores": "^1.2.0",
14+
"react": "^19.2.4",
15+
"react-dom": "^19.2.4",
16+
"react-native": "^0.84.1",
17+
"react-native-web": "^0.21.2"
18+
}
19+
}

0 commit comments

Comments
 (0)