@livestore/adapter-expo 0.4.0-dev.9 → 0.5.0-dev.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 +82 -31
- package/dist/index.js.map +1 -1
- package/dist/make-sqlite-db.d.ts.map +1 -1
- package/dist/make-sqlite-db.js +1 -5
- 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 +52 -18
- package/src/index.ts +112 -43
- package/src/make-sqlite-db.ts +1 -5
- package/src/polyfill.ts +2 -1
package/src/index.ts
CHANGED
|
@@ -9,13 +9,19 @@ import {
|
|
|
9
9
|
type LockStatus,
|
|
10
10
|
liveStoreStorageFormatVersion,
|
|
11
11
|
makeClientSession,
|
|
12
|
+
StateHead,
|
|
12
13
|
type SyncOptions,
|
|
13
|
-
|
|
14
|
+
UnknownError,
|
|
14
15
|
} from '@livestore/common'
|
|
15
16
|
import type { DevtoolsOptions, LeaderSqliteDb } from '@livestore/common/leader-thread'
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
Eventlog,
|
|
19
|
+
LeaderThreadCtx,
|
|
20
|
+
makeLeaderThreadLayer,
|
|
21
|
+
streamEventsWithSyncState,
|
|
22
|
+
} from '@livestore/common/leader-thread'
|
|
17
23
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
18
|
-
import { LiveStoreEvent } from '@livestore/common/schema'
|
|
24
|
+
import { getStateDbBaseName, LiveStoreEvent } from '@livestore/common/schema'
|
|
19
25
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
20
26
|
import type { Schema, Scope } from '@livestore/utils/effect'
|
|
21
27
|
import {
|
|
@@ -42,7 +48,12 @@ export type MakeDbOptions = {
|
|
|
42
48
|
sync?: SyncOptions
|
|
43
49
|
storage?: {
|
|
44
50
|
/**
|
|
45
|
-
*
|
|
51
|
+
* The directory where the database file is located.
|
|
52
|
+
* @default SQLite.defaultDatabaseDirectory
|
|
53
|
+
*/
|
|
54
|
+
directory?: string
|
|
55
|
+
/**
|
|
56
|
+
* Sub-directory relative to the configured `directory` (or expo-sqlite's default directory if not specified).
|
|
46
57
|
*
|
|
47
58
|
* Example of a resulting path for `subDirectory: 'my-app'`:
|
|
48
59
|
* `/data/Containers/Data/Application/<APP_UUID>/Documents/ExponentExperienceData/@<USERNAME>/<APPNAME>/SQLite/my-app/<STORE_ID>/livestore-eventlog@3.db`
|
|
@@ -62,26 +73,63 @@ export type MakeDbOptions = {
|
|
|
62
73
|
resetPersistence?: boolean
|
|
63
74
|
}
|
|
64
75
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
// Expo Go with the New Architecture enables Fabric and TurboModules, but may not run in "bridgeless" mode.
|
|
77
|
+
// Rely on Fabric/TurboModules feature detection instead of RN$Bridgeless.
|
|
78
|
+
const IS_NEW_ARCH =
|
|
79
|
+
// Fabric global – set when the new renderer is enabled
|
|
80
|
+
Boolean((globalThis as any).nativeFabricUIManager) ||
|
|
81
|
+
// TurboModule proxy – indicates new arch TurboModules
|
|
82
|
+
Boolean((globalThis as any).__turboModuleProxy)
|
|
70
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Creates a persisted LiveStore adapter for Expo/React Native applications.
|
|
86
|
+
*
|
|
87
|
+
* This adapter stores data in SQLite databases on the device filesystem, providing
|
|
88
|
+
* persistence across app restarts. It supports optional sync backends for multi-device
|
|
89
|
+
* synchronization.
|
|
90
|
+
*
|
|
91
|
+
* **Requirements:**
|
|
92
|
+
* - React Native New Architecture (Fabric) must be enabled
|
|
93
|
+
* - Expo SDK 51+ recommended
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* import { makePersistedAdapter } from '@livestore/adapter-expo'
|
|
98
|
+
* import { makeWsSync } from '@livestore/sync-cf/client'
|
|
99
|
+
*
|
|
100
|
+
* const adapter = makePersistedAdapter({
|
|
101
|
+
* sync: {
|
|
102
|
+
* backend: makeWsSync({ url: 'wss://api.example.com/sync' }),
|
|
103
|
+
* },
|
|
104
|
+
* storage: {
|
|
105
|
+
* subDirectory: 'my-app',
|
|
106
|
+
* },
|
|
107
|
+
* })
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* // Minimal setup without sync
|
|
113
|
+
* const adapter = makePersistedAdapter()
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @see https://livestore.dev/docs/reference/adapters/expo for detailed setup guide
|
|
117
|
+
*/
|
|
71
118
|
// TODO refactor with leader-thread code from `@livestore/common/leader-thread`
|
|
72
119
|
export const makePersistedAdapter =
|
|
73
120
|
(options: MakeDbOptions = {}): Adapter =>
|
|
74
121
|
(adapterArgs) =>
|
|
75
122
|
Effect.gen(function* () {
|
|
76
123
|
if (IS_NEW_ARCH === false) {
|
|
77
|
-
return yield*
|
|
124
|
+
return yield* UnknownError.make({
|
|
78
125
|
cause: new Error(
|
|
79
126
|
'The LiveStore Expo adapter requires the new React Native architecture (aka Fabric). See https://docs.expo.dev/guides/new-architecture',
|
|
80
127
|
),
|
|
81
128
|
})
|
|
82
129
|
}
|
|
83
130
|
|
|
84
|
-
const { schema, shutdown, devtoolsEnabled, storeId, bootStatusQueue,
|
|
131
|
+
const { schema, shutdown, devtoolsEnabled, storeId, bootStatusQueue, syncPayloadEncoded, syncPayloadSchema } =
|
|
132
|
+
adapterArgs
|
|
85
133
|
|
|
86
134
|
const {
|
|
87
135
|
storage,
|
|
@@ -104,9 +152,9 @@ export const makePersistedAdapter =
|
|
|
104
152
|
}
|
|
105
153
|
|
|
106
154
|
yield* shutdownChannel.listen.pipe(
|
|
107
|
-
Stream.
|
|
155
|
+
Stream.mapEffect(Effect.fromResult),
|
|
108
156
|
Stream.tap((cause) =>
|
|
109
|
-
shutdown(cause._tag === '
|
|
157
|
+
shutdown(cause._tag === 'IntentionalShutdownCause' ? Exit.succeed(cause) : Exit.fail(cause)),
|
|
110
158
|
),
|
|
111
159
|
Stream.runDrain,
|
|
112
160
|
Effect.interruptible,
|
|
@@ -114,7 +162,7 @@ export const makePersistedAdapter =
|
|
|
114
162
|
Effect.forkScoped,
|
|
115
163
|
)
|
|
116
164
|
|
|
117
|
-
const devtoolsUrl = getDevtoolsUrl().toString()
|
|
165
|
+
const devtoolsUrl = devtoolsEnabled === true ? getDevtoolsUrl().toString() : 'ws://127.0.0.1:4242'
|
|
118
166
|
|
|
119
167
|
const { leaderThread, initialSnapshot } = yield* makeLeaderThread({
|
|
120
168
|
storeId,
|
|
@@ -125,11 +173,14 @@ export const makePersistedAdapter =
|
|
|
125
173
|
storage: storage ?? {},
|
|
126
174
|
devtoolsEnabled,
|
|
127
175
|
bootStatusQueue,
|
|
128
|
-
|
|
176
|
+
syncPayloadEncoded,
|
|
177
|
+
syncPayloadSchema,
|
|
129
178
|
devtoolsUrl,
|
|
130
179
|
})
|
|
131
180
|
|
|
132
|
-
const sqliteDb = yield* makeSqliteDb({ _tag: 'in-memory' })
|
|
181
|
+
const sqliteDb = yield* Effect.acquireRelease(makeSqliteDb({ _tag: 'in-memory' }), (db) =>
|
|
182
|
+
Effect.try(() => db.close()).pipe(Effect.ignore),
|
|
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.Json | undefined
|
|
242
|
+
syncPayloadSchema: Schema.Decoder<Schema.Json> | 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.ignore),
|
|
255
|
+
)
|
|
256
|
+
const dbEventlog = yield* Effect.acquireRelease(
|
|
257
|
+
makeSqliteDb({ _tag: 'file', databaseName: eventlogDatabaseName, directory }),
|
|
258
|
+
(db) => Effect.try(() => db.close()).pipe(Effect.ignore),
|
|
259
|
+
)
|
|
199
260
|
|
|
200
261
|
const devtoolsOptions = yield* makeDevtoolsOptions({
|
|
201
262
|
devtoolsEnabled,
|
|
@@ -206,7 +267,7 @@ const makeLeaderThread = ({
|
|
|
206
267
|
clientId,
|
|
207
268
|
})
|
|
208
269
|
|
|
209
|
-
const layer = yield* Layer.
|
|
270
|
+
const layer = yield* Layer.build(
|
|
210
271
|
makeLeaderThreadLayer({
|
|
211
272
|
clientId,
|
|
212
273
|
dbState,
|
|
@@ -218,8 +279,9 @@ const makeLeaderThread = ({
|
|
|
218
279
|
shutdownChannel: yield* makeShutdownChannel(storeId),
|
|
219
280
|
storeId,
|
|
220
281
|
syncOptions,
|
|
221
|
-
|
|
222
|
-
|
|
282
|
+
syncPayloadEncoded,
|
|
283
|
+
syncPayloadSchema,
|
|
284
|
+
}).pipe(Layer.provide(StateHead.layer({ dbState })), Layer.provideMerge(FetchHttpClient.layer)),
|
|
223
285
|
)
|
|
224
286
|
|
|
225
287
|
return yield* Effect.gen(function* () {
|
|
@@ -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(
|
|
@@ -239,7 +302,7 @@ const makeLeaderThread = ({
|
|
|
239
302
|
Effect.forkScoped,
|
|
240
303
|
)
|
|
241
304
|
|
|
242
|
-
yield* Queue.
|
|
305
|
+
yield* Queue.await(bootStatusQueueClientSession).pipe(
|
|
243
306
|
Effect.andThen(Fiber.interrupt(bootStatusFiber)),
|
|
244
307
|
Effect.tapCauseLogPretty,
|
|
245
308
|
Effect.forkScoped,
|
|
@@ -251,18 +314,24 @@ const makeLeaderThread = ({
|
|
|
251
314
|
events: {
|
|
252
315
|
pull: ({ cursor }) => syncProcessor.pull({ cursor }),
|
|
253
316
|
push: (batch) =>
|
|
254
|
-
syncProcessor
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
317
|
+
syncProcessor.push(batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item))),
|
|
318
|
+
stream: (options) =>
|
|
319
|
+
streamEventsWithSyncState({
|
|
320
|
+
dbEventlog,
|
|
321
|
+
syncState: syncProcessor.syncState,
|
|
322
|
+
options,
|
|
323
|
+
}),
|
|
324
|
+
},
|
|
325
|
+
initialState: {
|
|
326
|
+
leaderHead: initialLeaderHead,
|
|
327
|
+
migrationsReport: initialState.migrationsReport,
|
|
328
|
+
storageMode: 'persisted',
|
|
260
329
|
},
|
|
261
|
-
initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
|
|
262
330
|
export: Effect.sync(() => db.export()),
|
|
263
331
|
getEventlogData: Effect.sync(() => dbEventlog.export()),
|
|
264
|
-
|
|
265
|
-
sendDevtoolsMessage: (message) =>
|
|
332
|
+
syncState: syncProcessor.syncState,
|
|
333
|
+
sendDevtoolsMessage: (message) => Queue.offer(extraIncomingMessagesQueue, message),
|
|
334
|
+
networkStatus,
|
|
266
335
|
})
|
|
267
336
|
|
|
268
337
|
const initialSnapshot = db.export()
|
|
@@ -277,16 +346,16 @@ const resolveExpoPersistencePaths = ({
|
|
|
277
346
|
schema,
|
|
278
347
|
}: {
|
|
279
348
|
storeId: string
|
|
280
|
-
storage: { subDirectory?: string } | undefined
|
|
281
349
|
schema: LiveStoreSchema
|
|
350
|
+
storage: { directory?: string; subDirectory?: string } | undefined
|
|
282
351
|
}) => {
|
|
283
|
-
const subDirectory = storage?.subDirectory ? `${storage.subDirectory.replace(/\/$/, '')}/` : ''
|
|
352
|
+
const subDirectory = storage?.subDirectory !== undefined ? `${storage.subDirectory.replace(/\/$/, '')}/` : ''
|
|
284
353
|
const pathJoin = (...paths: string[]) => paths.join('/').replaceAll(/\/+/g, '/')
|
|
285
|
-
const
|
|
354
|
+
const directoryBasePath = storage?.directory ?? SQLite.defaultDatabaseDirectory
|
|
355
|
+
|
|
356
|
+
const directory = pathJoin(directoryBasePath, subDirectory, storeId)
|
|
286
357
|
|
|
287
|
-
const
|
|
288
|
-
schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
|
|
289
|
-
const stateDatabaseName = `livestore-${schemaHashSuffix}@${liveStoreStorageFormatVersion}.db`
|
|
358
|
+
const stateDatabaseName = `${getStateDbBaseName(schema)}@${liveStoreStorageFormatVersion}.db`
|
|
290
359
|
const eventlogDatabaseName = `livestore-eventlog@${liveStoreStorageFormatVersion}.db`
|
|
291
360
|
|
|
292
361
|
return { directory, stateDatabaseName, eventlogDatabaseName }
|
|
@@ -313,7 +382,7 @@ const resetExpoPersistence = ({
|
|
|
313
382
|
SQLite.deleteDatabaseSync(eventlogDatabaseName, directory)
|
|
314
383
|
},
|
|
315
384
|
catch: (cause) =>
|
|
316
|
-
new
|
|
385
|
+
new UnknownError({
|
|
317
386
|
cause,
|
|
318
387
|
note: `@livestore/adapter-expo: Failed to reset persistence for store ${storeId}`,
|
|
319
388
|
}),
|
|
@@ -336,7 +405,7 @@ const makeDevtoolsOptions = ({
|
|
|
336
405
|
dbEventlog: LeaderSqliteDb
|
|
337
406
|
storeId: string
|
|
338
407
|
clientId: string
|
|
339
|
-
}): Effect.Effect<DevtoolsOptions,
|
|
408
|
+
}): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope> =>
|
|
340
409
|
Effect.sync(() => {
|
|
341
410
|
if (devtoolsEnabled === false) {
|
|
342
411
|
return {
|
package/src/make-sqlite-db.ts
CHANGED
|
@@ -9,10 +9,6 @@ import {
|
|
|
9
9
|
import { EventSequenceNumber } from '@livestore/common/schema'
|
|
10
10
|
import { ensureUint8ArrayBuffer, shouldNeverHappen } from '@livestore/utils'
|
|
11
11
|
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
12
|
import * as SQLite from 'expo-sqlite'
|
|
17
13
|
|
|
18
14
|
type Metadata = {
|
|
@@ -80,7 +76,7 @@ const makeSqliteDb_ = <TMetadata extends Metadata>({
|
|
|
80
76
|
_tag: 'SqliteDb',
|
|
81
77
|
debug: {
|
|
82
78
|
// Setting initially to root but will be set to correct value shortly after
|
|
83
|
-
head: EventSequenceNumber.ROOT,
|
|
79
|
+
head: EventSequenceNumber.Client.ROOT,
|
|
84
80
|
},
|
|
85
81
|
prepare: (queryStr) => {
|
|
86
82
|
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
|
}
|