Skip to content

Commit

Permalink
add MMKV & friends, store-native implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
boorad committed May 2, 2024
1 parent 062157d commit 92eaadd
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 11 deletions.
20 changes: 18 additions & 2 deletions examples/react-native/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ PODS:
- hermes-engine/Pre-built (= 0.73.7)
- hermes-engine/Pre-built (0.73.7)
- libevent (2.1.12)
- MMKV (1.3.4):
- MMKVCore (~> 1.3.4)
- MMKVCore (1.3.4)
- OpenSSL-Universal (3.1.5001)
- RCT-Folly (2022.05.16.00):
- boost
Expand Down Expand Up @@ -891,11 +894,16 @@ PODS:
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
- react-native-mmkv (2.12.2):
- glog
- MMKV (>= 1.3.3)
- RCT-Folly (= 2022.05.16.00)
- React-Core
- react-native-quick-base64 (2.1.2):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
- react-native-quick-crypto (0.7.0-rc.3):
- react-native-quick-crypto (0.7.0-rc.4):
- OpenSSL-Universal
- React
- React-callinvoker
Expand Down Expand Up @@ -1103,6 +1111,7 @@ DEPENDENCIES:
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- react-native-fast-encoder (from `../node_modules/react-native-fast-encoder`)
- react-native-mmkv (from `../node_modules/react-native-mmkv`)
- react-native-quick-base64 (from `../node_modules/react-native-quick-base64`)
- react-native-quick-crypto (from `../node_modules/react-native-quick-crypto`)
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
Expand Down Expand Up @@ -1131,6 +1140,8 @@ SPEC REPOS:
trunk:
- fmt
- libevent
- MMKV
- MMKVCore
- OpenSSL-Universal
- SocketRocket

Expand Down Expand Up @@ -1192,6 +1203,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon"
react-native-fast-encoder:
:path: "../node_modules/react-native-fast-encoder"
react-native-mmkv:
:path: "../node_modules/react-native-mmkv"
react-native-quick-base64:
:path: "../node_modules/react-native-quick-base64"
react-native-quick-crypto:
Expand Down Expand Up @@ -1248,6 +1261,8 @@ SPEC CHECKSUMS:
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
hermes-engine: 39589e9c297d024e90fe68f6830ff86c4e01498a
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
MMKV: ed58ad794b3f88c24d604a5b74f3fba17fcbaf74
MMKVCore: a67a1cede26175c413176f404a7cedec43f96a0b
OpenSSL-Universal: 29a9c9d4baf23f5fcd1294b657e4cc275e605bc3
RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0
RCTRequired: 77f73950d15b8c1a2b48ba5b79020c3003d1c9b5
Expand All @@ -1271,8 +1286,9 @@ SPEC CHECKSUMS:
React-logger: 7b19bdfb254772a0332d6cd4d66eceb0678b6730
React-Mapbuffer: 6f392912435adb8fbf4c3eee0e79a0a0b4e4b717
react-native-fast-encoder: 594ecf3598f181895a72d4c4cc8704ec0db0c8e2
react-native-mmkv: 1fdc81aa70c1aba09370718e6a63a09cbbbac8d2
react-native-quick-base64: e1ea036b3dec44c6da2439bd62881a09de614b23
react-native-quick-crypto: c73e268be62700b450994324c7203ff9b39e5f5f
react-native-quick-crypto: 416e74d9a364587b3b79972e0f0d7f2c653c7fb3
React-nativeconfig: 754233aac2a769578f828093b672b399355582e6
React-NativeModulesApple: a03b2da2b8e127d5f5ee29c683e0deba7a9e1575
React-perflogger: 68ec84e2f858a3e35009aef8866b55893e5e0a1f
Expand Down
1 change: 1 addition & 0 deletions examples/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"react": "18.2.0",
"react-native": "0.73.7",
"react-native-fast-encoder": "^0.1.12",
"react-native-mmkv": "<3",
"react-native-quick-base64": "^2.1.2",
"react-native-quick-crypto": "link:../../../react-native-quick-crypto"
},
Expand Down
3 changes: 2 additions & 1 deletion examples/react-native/src/TodoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const TodoList = () => {
const { useDocument, useLiveQuery } = useFireproof('TodoDB', {public: true});
// const [selectedTodo, setSelectedTodo] = useState<string>("")
const todos: Doc<Todo>[] = useLiveQuery<Todo>('date', {limit: 10, descending: true}).docs;
console.log({todos});
const [todo, setTodo, saveTodo] = useDocument<Todo>(() => ({
// TODO: reset to '' after dev work
text: 'implement mmkv as backend',
Expand All @@ -26,7 +27,7 @@ const TodoList = () => {
}));

const TodoItem = ({item, index}) => {
// console.log({item});
// console.log({item, index});
return (
<View key={index}>
<Switch
Expand Down
2 changes: 1 addition & 1 deletion packages/encrypted-blockstore/src/encrypt-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { nocache as cache } from 'prolly-trees/cache'
import { create, load } from 'prolly-trees/cid-set'

import { encodeCarFile } from './loader-helpers'
import { makeCodec } from './encrypt-codec.js'
import { makeCodec } from './encrypt-codec'
import type { AnyBlock, CarMakeable, AnyLink, AnyDecodedBlock, CryptoOpts } from './types'

function makeEncDec(crypto: any, randomBytes: (size: number) => Uint8Array) {
Expand Down
12 changes: 7 additions & 5 deletions packages/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,18 @@
],
"exports": {
".": {
"import": "./src/index.js",
"types": "./src/index.d.ts",
"default": "./src/index.js"
"import": "./src/index.tsx",
"types": "./src/index.tsx",
"default": "./src/index.tsx"
},
"./polyfills": "./src/polyfills.js"
"./polyfills": "./src/polyfills.tsx"
},
"scripts": {
"prepublishOnly": "cp ../../README.md . && npm run build",
"postpublish": "rm README.md",
"build": "tsup",
"build:clean": "rm -rf dist",
"build:watch": "tsup --watch",
"build:watch:react": "pnpm build:watch",
"clean": "rm -rf node_modules",
"format:check": "prettier . --check",
"format:fix": "prettier . --write",
Expand All @@ -54,8 +53,11 @@
"gptdoc": "Fireproof/React Native/Usage: import { useLiveQuery, useDocument } from 'use-fireproof'; function App() { const result = useLiveQuery(doc => doc.word, { limit: 10 }); const [{ count }, setDoc, saveDoc] = useDocument({_id: 'count', count: 0}); return (<><p>{count} changes</p><input type='text' onChange={() => saveDoc({count: count + 1})} onSubmit={e => useLiveQuery.database.put({word: e.target.value})} /><ul>{result.map(row => (<li key={row.id}>{row.key}</li>))}</ul></>)}",
"dependencies": {
"@craftzdog/react-native-buffer": "^6.0.5",
"@fireproof/encrypted-blockstore": "workspace:^",
"@ipld/dag-json": "^10.1.2",
"event-target-shim": "^6.0.2",
"react-native-fast-encoder": "^0.1.12",
"react-native-mmkv": "<3",
"react-native-quick-base64": "^2.1.2",
"react-native-quick-crypto": "link:../../../react-native-quick-crypto",
"readable-stream": "^4.5.2",
Expand Down
2 changes: 0 additions & 2 deletions packages/react-native/src/index.js

This file was deleted.

32 changes: 32 additions & 0 deletions packages/react-native/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ConfigOpts, Database, useFireproof as useFireproofReact } from 'use-fireproof';
import { StoreOpts } from '@fireproof/encrypted-blockstore'
import * as crypto from '@fireproof/encrypted-blockstore/crypto-web'

import {
makeDataStore,
makeMetaStore,
makeRemoteWAL
} from './store-native'

const store = {
makeDataStore,
makeMetaStore,
makeRemoteWAL
} as unknown as StoreOpts


// Fireproof React exports
export * from 'use-fireproof';

// export (override with) a new 'useFireproof' for React Native
export const useFireproof = (
name?: string | Database | undefined,
config?: ConfigOpts | undefined
) => {
return useFireproofReact(name, {
...config,
store,
crypto,
})
};

File renamed without changes.
131 changes: 131 additions & 0 deletions packages/react-native/src/store-native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/* eslint-disable import/first */
import { format, parse, ToString } from '@ipld/dag-json'
import { MMKV } from 'react-native-mmkv'
import { AnyBlock, AnyLink, DbMeta } from '@fireproof/encrypted-blockstore/src/types'
import { DataStore as DataStoreBase, MetaStore as MetaStoreBase } from '@fireproof/encrypted-blockstore/src/store'
import { RemoteWAL as RemoteWALBase, WALState } from '@fireproof/encrypted-blockstore/src/remote-wal'
import type { Loadable, Loader } from '@fireproof/encrypted-blockstore/src/loader'

export const makeDataStore = (name: string) => new DataStore(name)
export const makeMetaStore = (loader: Loader) => new MetaStore(loader.name)
export const makeRemoteWAL = (loader: Loadable) => new RemoteWAL(loader)

export class DataStore extends DataStoreBase {
tag: string = 'car-native-mmkv'
rndb: MMKV | null = null

async _withDB(dbWorkFun: (arg0: any) => any) {
if (!this.rndb) {
const dbName = `fp.${this.STORAGE_VERSION}.${this.name}`
this.rndb = new MMKV({
id: dbName,
})
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await dbWorkFun(this.rndb)
}

async load(cid: AnyLink): Promise<AnyBlock> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await this._withDB(async (db: MMKV) => {
const bytes = db.getBuffer(cid.toString())
if (!bytes) throw new Error(`missing db block ${cid.toString()}`)
return { cid, bytes }
})
}

async save(car: AnyBlock): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await this._withDB(async (db: MMKV) => {
db.set(car.cid.toString(), car.bytes)
})
}

async remove(cid: AnyLink): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await this._withDB(async (db: MMKV) => {
db.delete(cid.toString())
})
}
}

export class RemoteWAL extends RemoteWALBase {
tag: string = 'wal-native-mmkv'
wal: MMKV | null = null

headerKey(branch: string) {
// remove 'public' on next storage version bump
return `fp.${this.STORAGE_VERSION}.wal.${this.loader.name}.${branch}`
}

async _withDB(dbWorkFun: (arg0: any) => any) {
if (!this.wal) {
const dbName = `fp.${this.STORAGE_VERSION}.wal`
this.wal = new MMKV({
id: dbName,
})
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await dbWorkFun(this.wal)
}

// eslint-disable-next-line @typescript-eslint/require-await
async load(branch = 'main'): Promise<WALState | null> {
return await this._withDB(async (db: MMKV) => {
const doc = db.getString(this.headerKey(branch))
if (!doc) return null
return parse<WALState>(doc)
})
}

// eslint-disable-next-line @typescript-eslint/require-await
async save(state: WALState, branch = 'main'): Promise<void> {
return await this._withDB(async (db: MMKV) => {
const encoded: ToString<WALState> = format(state)
db.set(this.headerKey(branch), encoded)
})
}
}

export class MetaStore extends MetaStoreBase {
tag: string = 'header-native-mmkv'
meta: MMKV | null = null

headerKey(branch: string) {
return `fp.${this.STORAGE_VERSION}.meta.${this.name}.${branch}`
}

async _withDB(dbWorkFun: (arg0: any) => any) {
if (!this.meta) {
const dbName = `fp.${this.STORAGE_VERSION}.meta`
this.meta = new MMKV({
id: dbName,
})
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await dbWorkFun(this.meta)
}

// eslint-disable-next-line @typescript-eslint/require-await
async load(branch: string = 'main'): Promise<DbMeta[] | null> {
return await this._withDB(async (db: MMKV) => {
const doc = db.getString(this.headerKey(branch))
if (!doc) return null
// TODO: react native wrt below?
// browser assumes a single writer process
// to support concurrent updates to the same database across multiple tabs
// we need to implement the same kind of mvcc.crdt solution as in store-fs and connect-s3
return [this.parseHeader(doc)]
})
}

// eslint-disable-next-line @typescript-eslint/require-await
async save(meta: DbMeta, branch: string = 'main') {
return await this._withDB(async (db: MMKV) => {
const headerKey = this.headerKey(branch)
const bytes = this.makeHeader(meta)
db.set(headerKey, bytes)
return null
})
}
}
23 changes: 23 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 92eaadd

Please sign in to comment.