@livestore/adapter-expo 0.4.0-dev.9 → 0.4.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +40 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -26
- package/dist/index.js.map +1 -1
- package/dist/make-sqlite-db.d.ts.map +1 -1
- package/dist/make-sqlite-db.js +2 -6
- package/dist/make-sqlite-db.js.map +1 -1
- package/dist/polyfill.js +2 -1
- package/dist/polyfill.js.map +1 -1
- package/dist/shutdown-channel.d.ts +1 -1
- package/dist/shutdown-channel.d.ts.map +1 -1
- package/package.json +69 -17
- package/src/index.ts +109 -34
- package/src/make-sqlite-db.ts +3 -6
- package/src/polyfill.ts +2 -1
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import './polyfill.ts'
|
|
2
|
+
import * as ExpoApplication from 'expo-application'
|
|
3
|
+
import * as SQLite from 'expo-sqlite'
|
|
4
|
+
import * as RN from 'react-native'
|
|
2
5
|
|
|
3
6
|
import {
|
|
4
7
|
type Adapter,
|
|
@@ -10,10 +13,15 @@ import {
|
|
|
10
13
|
liveStoreStorageFormatVersion,
|
|
11
14
|
makeClientSession,
|
|
12
15
|
type SyncOptions,
|
|
13
|
-
|
|
16
|
+
UnknownError,
|
|
14
17
|
} from '@livestore/common'
|
|
15
18
|
import type { DevtoolsOptions, LeaderSqliteDb } from '@livestore/common/leader-thread'
|
|
16
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
Eventlog,
|
|
21
|
+
LeaderThreadCtx,
|
|
22
|
+
makeLeaderThreadLayer,
|
|
23
|
+
streamEventsWithSyncState,
|
|
24
|
+
} from '@livestore/common/leader-thread'
|
|
17
25
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
18
26
|
import { LiveStoreEvent } from '@livestore/common/schema'
|
|
19
27
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
@@ -30,9 +38,6 @@ import {
|
|
|
30
38
|
SubscriptionRef,
|
|
31
39
|
} from '@livestore/utils/effect'
|
|
32
40
|
import * as Webmesh from '@livestore/webmesh'
|
|
33
|
-
import * as ExpoApplication from 'expo-application'
|
|
34
|
-
import * as SQLite from 'expo-sqlite'
|
|
35
|
-
import * as RN from 'react-native'
|
|
36
41
|
|
|
37
42
|
import type { MakeExpoSqliteDb } from './make-sqlite-db.ts'
|
|
38
43
|
import { makeSqliteDb } from './make-sqlite-db.ts'
|
|
@@ -42,7 +47,12 @@ export type MakeDbOptions = {
|
|
|
42
47
|
sync?: SyncOptions
|
|
43
48
|
storage?: {
|
|
44
49
|
/**
|
|
45
|
-
*
|
|
50
|
+
* The directory where the database file is located.
|
|
51
|
+
* @default SQLite.defaultDatabaseDirectory
|
|
52
|
+
*/
|
|
53
|
+
directory?: string
|
|
54
|
+
/**
|
|
55
|
+
* Sub-directory relative to the configured `directory` (or expo-sqlite's default directory if not specified).
|
|
46
56
|
*
|
|
47
57
|
* Example of a resulting path for `subDirectory: 'my-app'`:
|
|
48
58
|
* `/data/Containers/Data/Application/<APP_UUID>/Documents/ExponentExperienceData/@<USERNAME>/<APPNAME>/SQLite/my-app/<STORE_ID>/livestore-eventlog@3.db`
|
|
@@ -62,26 +72,63 @@ export type MakeDbOptions = {
|
|
|
62
72
|
resetPersistence?: boolean
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
// Expo Go with the New Architecture enables Fabric and TurboModules, but may not run in "bridgeless" mode.
|
|
76
|
+
// Rely on Fabric/TurboModules feature detection instead of RN$Bridgeless.
|
|
77
|
+
const IS_NEW_ARCH =
|
|
78
|
+
// Fabric global – set when the new renderer is enabled
|
|
79
|
+
Boolean((globalThis as any).nativeFabricUIManager) ||
|
|
80
|
+
// TurboModule proxy – indicates new arch TurboModules
|
|
81
|
+
Boolean((globalThis as any).__turboModuleProxy)
|
|
70
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Creates a persisted LiveStore adapter for Expo/React Native applications.
|
|
85
|
+
*
|
|
86
|
+
* This adapter stores data in SQLite databases on the device filesystem, providing
|
|
87
|
+
* persistence across app restarts. It supports optional sync backends for multi-device
|
|
88
|
+
* synchronization.
|
|
89
|
+
*
|
|
90
|
+
* **Requirements:**
|
|
91
|
+
* - React Native New Architecture (Fabric) must be enabled
|
|
92
|
+
* - Expo SDK 51+ recommended
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { makePersistedAdapter } from '@livestore/adapter-expo'
|
|
97
|
+
* import { makeWsSync } from '@livestore/sync-cf/client'
|
|
98
|
+
*
|
|
99
|
+
* const adapter = makePersistedAdapter({
|
|
100
|
+
* sync: {
|
|
101
|
+
* backend: makeWsSync({ url: 'wss://api.example.com/sync' }),
|
|
102
|
+
* },
|
|
103
|
+
* storage: {
|
|
104
|
+
* subDirectory: 'my-app',
|
|
105
|
+
* },
|
|
106
|
+
* })
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* // Minimal setup without sync
|
|
112
|
+
* const adapter = makePersistedAdapter()
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* @see https://livestore.dev/docs/reference/adapters/expo for detailed setup guide
|
|
116
|
+
*/
|
|
71
117
|
// TODO refactor with leader-thread code from `@livestore/common/leader-thread`
|
|
72
118
|
export const makePersistedAdapter =
|
|
73
119
|
(options: MakeDbOptions = {}): Adapter =>
|
|
74
120
|
(adapterArgs) =>
|
|
75
121
|
Effect.gen(function* () {
|
|
76
122
|
if (IS_NEW_ARCH === false) {
|
|
77
|
-
return yield*
|
|
123
|
+
return yield* UnknownError.make({
|
|
78
124
|
cause: new Error(
|
|
79
125
|
'The LiveStore Expo adapter requires the new React Native architecture (aka Fabric). See https://docs.expo.dev/guides/new-architecture',
|
|
80
126
|
),
|
|
81
127
|
})
|
|
82
128
|
}
|
|
83
129
|
|
|
84
|
-
const { schema, shutdown, devtoolsEnabled, storeId, bootStatusQueue,
|
|
130
|
+
const { schema, shutdown, devtoolsEnabled, storeId, bootStatusQueue, syncPayloadEncoded, syncPayloadSchema } =
|
|
131
|
+
adapterArgs
|
|
85
132
|
|
|
86
133
|
const {
|
|
87
134
|
storage,
|
|
@@ -106,7 +153,7 @@ export const makePersistedAdapter =
|
|
|
106
153
|
yield* shutdownChannel.listen.pipe(
|
|
107
154
|
Stream.flatten(),
|
|
108
155
|
Stream.tap((cause) =>
|
|
109
|
-
shutdown(cause._tag === '
|
|
156
|
+
shutdown(cause._tag === 'IntentionalShutdownCause' ? Exit.succeed(cause) : Exit.fail(cause)),
|
|
110
157
|
),
|
|
111
158
|
Stream.runDrain,
|
|
112
159
|
Effect.interruptible,
|
|
@@ -114,7 +161,7 @@ export const makePersistedAdapter =
|
|
|
114
161
|
Effect.forkScoped,
|
|
115
162
|
)
|
|
116
163
|
|
|
117
|
-
const devtoolsUrl = getDevtoolsUrl().toString()
|
|
164
|
+
const devtoolsUrl = devtoolsEnabled === true ? getDevtoolsUrl().toString() : 'ws://127.0.0.1:4242'
|
|
118
165
|
|
|
119
166
|
const { leaderThread, initialSnapshot } = yield* makeLeaderThread({
|
|
120
167
|
storeId,
|
|
@@ -125,11 +172,15 @@ export const makePersistedAdapter =
|
|
|
125
172
|
storage: storage ?? {},
|
|
126
173
|
devtoolsEnabled,
|
|
127
174
|
bootStatusQueue,
|
|
128
|
-
|
|
175
|
+
syncPayloadEncoded,
|
|
176
|
+
syncPayloadSchema,
|
|
129
177
|
devtoolsUrl,
|
|
130
178
|
})
|
|
131
179
|
|
|
132
|
-
const sqliteDb = yield*
|
|
180
|
+
const sqliteDb = yield* Effect.acquireRelease(
|
|
181
|
+
makeSqliteDb({ _tag: 'in-memory' }),
|
|
182
|
+
(db) => Effect.try(() => db.close()).pipe(Effect.ignoreLogged)
|
|
183
|
+
)
|
|
133
184
|
sqliteDb.import(initialSnapshot)
|
|
134
185
|
|
|
135
186
|
const clientSession = yield* makeClientSession({
|
|
@@ -142,7 +193,7 @@ export const makePersistedAdapter =
|
|
|
142
193
|
sqliteDb,
|
|
143
194
|
webmeshMode: 'proxy',
|
|
144
195
|
connectWebmeshNode: Effect.fnUntraced(function* ({ webmeshNode }) {
|
|
145
|
-
if (devtoolsEnabled) {
|
|
196
|
+
if (devtoolsEnabled === true) {
|
|
146
197
|
yield* Webmesh.connectViaWebSocket({
|
|
147
198
|
node: webmeshNode,
|
|
148
199
|
url: devtoolsUrl,
|
|
@@ -157,10 +208,11 @@ export const makePersistedAdapter =
|
|
|
157
208
|
|
|
158
209
|
return () => {}
|
|
159
210
|
},
|
|
211
|
+
origin: undefined,
|
|
160
212
|
})
|
|
161
213
|
|
|
162
214
|
return clientSession
|
|
163
|
-
}).pipe(
|
|
215
|
+
}).pipe(UnknownError.mapToUnknownError, Effect.provide(FetchHttpClient.layer), Effect.tapCauseLogPretty)
|
|
164
216
|
|
|
165
217
|
const makeLeaderThread = ({
|
|
166
218
|
storeId,
|
|
@@ -171,7 +223,8 @@ const makeLeaderThread = ({
|
|
|
171
223
|
storage,
|
|
172
224
|
devtoolsEnabled,
|
|
173
225
|
bootStatusQueue: bootStatusQueueClientSession,
|
|
174
|
-
|
|
226
|
+
syncPayloadEncoded,
|
|
227
|
+
syncPayloadSchema,
|
|
175
228
|
devtoolsUrl,
|
|
176
229
|
}: {
|
|
177
230
|
storeId: string
|
|
@@ -180,11 +233,13 @@ const makeLeaderThread = ({
|
|
|
180
233
|
makeSqliteDb: MakeExpoSqliteDb
|
|
181
234
|
syncOptions: SyncOptions | undefined
|
|
182
235
|
storage: {
|
|
236
|
+
directory?: string
|
|
183
237
|
subDirectory?: string
|
|
184
238
|
}
|
|
185
239
|
devtoolsEnabled: boolean
|
|
186
240
|
bootStatusQueue: Queue.Queue<BootStatus>
|
|
187
|
-
|
|
241
|
+
syncPayloadEncoded: Schema.JsonValue | undefined
|
|
242
|
+
syncPayloadSchema: Schema.Schema<any> | undefined
|
|
188
243
|
devtoolsUrl: string
|
|
189
244
|
}) =>
|
|
190
245
|
Effect.gen(function* () {
|
|
@@ -194,8 +249,14 @@ const makeLeaderThread = ({
|
|
|
194
249
|
schema,
|
|
195
250
|
})
|
|
196
251
|
|
|
197
|
-
const dbState = yield*
|
|
198
|
-
|
|
252
|
+
const dbState = yield* Effect.acquireRelease(
|
|
253
|
+
makeSqliteDb({ _tag: 'file', databaseName: stateDatabaseName, directory }),
|
|
254
|
+
(db) => Effect.try(() => db.close()).pipe(Effect.ignoreLogged),
|
|
255
|
+
)
|
|
256
|
+
const dbEventlog = yield* Effect.acquireRelease(
|
|
257
|
+
makeSqliteDb({ _tag: 'file', databaseName: eventlogDatabaseName, directory }),
|
|
258
|
+
(db) => Effect.try(() => db.close()).pipe(Effect.ignoreLogged),
|
|
259
|
+
)
|
|
199
260
|
|
|
200
261
|
const devtoolsOptions = yield* makeDevtoolsOptions({
|
|
201
262
|
devtoolsEnabled,
|
|
@@ -218,7 +279,8 @@ const makeLeaderThread = ({
|
|
|
218
279
|
shutdownChannel: yield* makeShutdownChannel(storeId),
|
|
219
280
|
storeId,
|
|
220
281
|
syncOptions,
|
|
221
|
-
|
|
282
|
+
syncPayloadEncoded,
|
|
283
|
+
syncPayloadSchema,
|
|
222
284
|
}).pipe(Layer.provideMerge(FetchHttpClient.layer)),
|
|
223
285
|
)
|
|
224
286
|
|
|
@@ -230,6 +292,7 @@ const makeLeaderThread = ({
|
|
|
230
292
|
extraIncomingMessagesQueue,
|
|
231
293
|
initialState,
|
|
232
294
|
bootStatusQueue,
|
|
295
|
+
networkStatus,
|
|
233
296
|
} = yield* LeaderThreadCtx
|
|
234
297
|
|
|
235
298
|
const bootStatusFiber = yield* Queue.takeBetween(bootStatusQueue, 1, 1000).pipe(
|
|
@@ -253,16 +316,26 @@ const makeLeaderThread = ({
|
|
|
253
316
|
push: (batch) =>
|
|
254
317
|
syncProcessor
|
|
255
318
|
.push(
|
|
256
|
-
batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
|
|
319
|
+
batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)),
|
|
257
320
|
{ waitForProcessing: true },
|
|
258
|
-
)
|
|
259
|
-
|
|
321
|
+
),
|
|
322
|
+
stream: (options) =>
|
|
323
|
+
streamEventsWithSyncState({
|
|
324
|
+
dbEventlog,
|
|
325
|
+
syncState: syncProcessor.syncState,
|
|
326
|
+
options,
|
|
327
|
+
}),
|
|
328
|
+
},
|
|
329
|
+
initialState: {
|
|
330
|
+
leaderHead: initialLeaderHead,
|
|
331
|
+
migrationsReport: initialState.migrationsReport,
|
|
332
|
+
storageMode: 'persisted',
|
|
260
333
|
},
|
|
261
|
-
initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
|
|
262
334
|
export: Effect.sync(() => db.export()),
|
|
263
335
|
getEventlogData: Effect.sync(() => dbEventlog.export()),
|
|
264
|
-
|
|
336
|
+
syncState: syncProcessor.syncState,
|
|
265
337
|
sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
|
|
338
|
+
networkStatus,
|
|
266
339
|
})
|
|
267
340
|
|
|
268
341
|
const initialSnapshot = db.export()
|
|
@@ -277,12 +350,14 @@ const resolveExpoPersistencePaths = ({
|
|
|
277
350
|
schema,
|
|
278
351
|
}: {
|
|
279
352
|
storeId: string
|
|
280
|
-
storage: { subDirectory?: string } | undefined
|
|
281
353
|
schema: LiveStoreSchema
|
|
354
|
+
storage: { directory?: string; subDirectory?: string } | undefined
|
|
282
355
|
}) => {
|
|
283
|
-
const subDirectory = storage?.subDirectory ? `${storage.subDirectory.replace(/\/$/, '')}/` : ''
|
|
356
|
+
const subDirectory = storage?.subDirectory !== undefined ? `${storage.subDirectory.replace(/\/$/, '')}/` : ''
|
|
284
357
|
const pathJoin = (...paths: string[]) => paths.join('/').replaceAll(/\/+/g, '/')
|
|
285
|
-
const
|
|
358
|
+
const directoryBasePath = storage?.directory ?? SQLite.defaultDatabaseDirectory
|
|
359
|
+
|
|
360
|
+
const directory = pathJoin(directoryBasePath, subDirectory, storeId)
|
|
286
361
|
|
|
287
362
|
const schemaHashSuffix =
|
|
288
363
|
schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
|
|
@@ -313,7 +388,7 @@ const resetExpoPersistence = ({
|
|
|
313
388
|
SQLite.deleteDatabaseSync(eventlogDatabaseName, directory)
|
|
314
389
|
},
|
|
315
390
|
catch: (cause) =>
|
|
316
|
-
new
|
|
391
|
+
new UnknownError({
|
|
317
392
|
cause,
|
|
318
393
|
note: `@livestore/adapter-expo: Failed to reset persistence for store ${storeId}`,
|
|
319
394
|
}),
|
|
@@ -336,7 +411,7 @@ const makeDevtoolsOptions = ({
|
|
|
336
411
|
dbEventlog: LeaderSqliteDb
|
|
337
412
|
storeId: string
|
|
338
413
|
clientId: string
|
|
339
|
-
}): Effect.Effect<DevtoolsOptions,
|
|
414
|
+
}): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope> =>
|
|
340
415
|
Effect.sync(() => {
|
|
341
416
|
if (devtoolsEnabled === false) {
|
|
342
417
|
return {
|
package/src/make-sqlite-db.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as SQLite from 'expo-sqlite'
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
type MakeSqliteDb,
|
|
3
5
|
type PersistenceInfo,
|
|
@@ -9,11 +11,6 @@ import {
|
|
|
9
11
|
import { EventSequenceNumber } from '@livestore/common/schema'
|
|
10
12
|
import { ensureUint8ArrayBuffer, shouldNeverHappen } from '@livestore/utils'
|
|
11
13
|
import { Effect } from '@livestore/utils/effect'
|
|
12
|
-
// TODO remove `expo-file-system` dependency once expo-sqlite supports `import`
|
|
13
|
-
// // @ts-expect-error package misses `exports`
|
|
14
|
-
// import * as ExpoFs from 'expo-file-system/src/next'
|
|
15
|
-
// import * as ExpoFs from 'expo-file-system'
|
|
16
|
-
import * as SQLite from 'expo-sqlite'
|
|
17
14
|
|
|
18
15
|
type Metadata = {
|
|
19
16
|
_tag: 'file'
|
|
@@ -80,7 +77,7 @@ const makeSqliteDb_ = <TMetadata extends Metadata>({
|
|
|
80
77
|
_tag: 'SqliteDb',
|
|
81
78
|
debug: {
|
|
82
79
|
// Setting initially to root but will be set to correct value shortly after
|
|
83
|
-
head: EventSequenceNumber.ROOT,
|
|
80
|
+
head: EventSequenceNumber.Client.ROOT,
|
|
84
81
|
},
|
|
85
82
|
prepare: (queryStr) => {
|
|
86
83
|
try {
|
package/src/polyfill.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
if (typeof Array.prototype.toSorted === 'undefined') {
|
|
8
8
|
Array.prototype.toSorted = function (compareFn) {
|
|
9
|
-
|
|
9
|
+
// oxlint-disable-next-line unicorn/no-array-sort -- this is the toSorted polyfill itself
|
|
10
|
+
return this.slice().sort(compareFn)
|
|
10
11
|
}
|
|
11
12
|
}
|