@livestore/adapter-expo 0.4.0-dev.2 → 0.4.0-dev.21
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 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -22
- 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/shutdown-channel.d.ts +1 -1
- package/dist/shutdown-channel.d.ts.map +1 -1
- package/package.json +9 -11
- package/src/index.ts +161 -29
- package/src/make-sqlite-db.ts +1 -5
package/src/index.ts
CHANGED
|
@@ -5,19 +5,35 @@ import {
|
|
|
5
5
|
type BootStatus,
|
|
6
6
|
ClientSessionLeaderThreadProxy,
|
|
7
7
|
Devtools,
|
|
8
|
+
IntentionalShutdownCause,
|
|
8
9
|
type LockStatus,
|
|
9
10
|
liveStoreStorageFormatVersion,
|
|
10
11
|
makeClientSession,
|
|
11
12
|
type SyncOptions,
|
|
12
|
-
|
|
13
|
+
UnknownError,
|
|
13
14
|
} from '@livestore/common'
|
|
14
15
|
import type { DevtoolsOptions, LeaderSqliteDb } from '@livestore/common/leader-thread'
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
Eventlog,
|
|
18
|
+
LeaderThreadCtx,
|
|
19
|
+
makeLeaderThreadLayer,
|
|
20
|
+
streamEventsWithSyncState,
|
|
21
|
+
} from '@livestore/common/leader-thread'
|
|
16
22
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
17
23
|
import { LiveStoreEvent } from '@livestore/common/schema'
|
|
18
24
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
19
25
|
import type { Schema, Scope } from '@livestore/utils/effect'
|
|
20
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
Effect,
|
|
28
|
+
Exit,
|
|
29
|
+
FetchHttpClient,
|
|
30
|
+
Fiber,
|
|
31
|
+
Layer,
|
|
32
|
+
Queue,
|
|
33
|
+
Schedule,
|
|
34
|
+
Stream,
|
|
35
|
+
SubscriptionRef,
|
|
36
|
+
} from '@livestore/utils/effect'
|
|
21
37
|
import * as Webmesh from '@livestore/webmesh'
|
|
22
38
|
import * as ExpoApplication from 'expo-application'
|
|
23
39
|
import * as SQLite from 'expo-sqlite'
|
|
@@ -43,30 +59,79 @@ export type MakeDbOptions = {
|
|
|
43
59
|
clientId?: string
|
|
44
60
|
/** @default 'static' */
|
|
45
61
|
sessionId?: string
|
|
62
|
+
/**
|
|
63
|
+
* Warning: This will reset both the app and eventlog database. This should only be used during development.
|
|
64
|
+
*
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
resetPersistence?: boolean
|
|
46
68
|
}
|
|
47
69
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
// Expo Go with the New Architecture enables Fabric and TurboModules, but may not run in "bridgeless" mode.
|
|
71
|
+
// Rely on Fabric/TurboModules feature detection instead of RN$Bridgeless.
|
|
72
|
+
const IS_NEW_ARCH =
|
|
73
|
+
// Fabric global – set when the new renderer is enabled
|
|
74
|
+
Boolean((globalThis as any).nativeFabricUIManager) ||
|
|
75
|
+
// TurboModule proxy – indicates new arch TurboModules
|
|
76
|
+
Boolean((globalThis as any).__turboModuleProxy)
|
|
53
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Creates a persisted LiveStore adapter for Expo/React Native applications.
|
|
80
|
+
*
|
|
81
|
+
* This adapter stores data in SQLite databases on the device filesystem, providing
|
|
82
|
+
* persistence across app restarts. It supports optional sync backends for multi-device
|
|
83
|
+
* synchronization.
|
|
84
|
+
*
|
|
85
|
+
* **Requirements:**
|
|
86
|
+
* - React Native New Architecture (Fabric) must be enabled
|
|
87
|
+
* - Expo SDK 51+ recommended
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { makePersistedAdapter } from '@livestore/adapter-expo'
|
|
92
|
+
* import { makeWsSync } from '@livestore/sync-cf/client'
|
|
93
|
+
*
|
|
94
|
+
* const adapter = makePersistedAdapter({
|
|
95
|
+
* sync: {
|
|
96
|
+
* backend: makeWsSync({ url: 'wss://api.example.com/sync' }),
|
|
97
|
+
* },
|
|
98
|
+
* storage: {
|
|
99
|
+
* subDirectory: 'my-app',
|
|
100
|
+
* },
|
|
101
|
+
* })
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* // Minimal setup without sync
|
|
107
|
+
* const adapter = makePersistedAdapter()
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @see https://livestore.dev/docs/reference/adapters/expo for detailed setup guide
|
|
111
|
+
*/
|
|
54
112
|
// TODO refactor with leader-thread code from `@livestore/common/leader-thread`
|
|
55
113
|
export const makePersistedAdapter =
|
|
56
114
|
(options: MakeDbOptions = {}): Adapter =>
|
|
57
115
|
(adapterArgs) =>
|
|
58
116
|
Effect.gen(function* () {
|
|
59
117
|
if (IS_NEW_ARCH === false) {
|
|
60
|
-
return yield*
|
|
118
|
+
return yield* UnknownError.make({
|
|
61
119
|
cause: new Error(
|
|
62
120
|
'The LiveStore Expo adapter requires the new React Native architecture (aka Fabric). See https://docs.expo.dev/guides/new-architecture',
|
|
63
121
|
),
|
|
64
122
|
})
|
|
65
123
|
}
|
|
66
124
|
|
|
67
|
-
const { schema, shutdown, devtoolsEnabled, storeId, bootStatusQueue,
|
|
125
|
+
const { schema, shutdown, devtoolsEnabled, storeId, bootStatusQueue, syncPayloadEncoded, syncPayloadSchema } =
|
|
126
|
+
adapterArgs
|
|
68
127
|
|
|
69
|
-
const {
|
|
128
|
+
const {
|
|
129
|
+
storage,
|
|
130
|
+
clientId = yield* getDeviceId,
|
|
131
|
+
sessionId = 'static',
|
|
132
|
+
sync: syncOptions,
|
|
133
|
+
resetPersistence = false,
|
|
134
|
+
} = options
|
|
70
135
|
|
|
71
136
|
yield* Queue.offer(bootStatusQueue, { stage: 'loading' })
|
|
72
137
|
|
|
@@ -74,6 +139,12 @@ export const makePersistedAdapter =
|
|
|
74
139
|
|
|
75
140
|
const shutdownChannel = yield* makeShutdownChannel(storeId)
|
|
76
141
|
|
|
142
|
+
if (resetPersistence === true) {
|
|
143
|
+
yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'adapter-reset' }))
|
|
144
|
+
|
|
145
|
+
yield* resetExpoPersistence({ storeId, storage, schema })
|
|
146
|
+
}
|
|
147
|
+
|
|
77
148
|
yield* shutdownChannel.listen.pipe(
|
|
78
149
|
Stream.flatten(),
|
|
79
150
|
Stream.tap((cause) =>
|
|
@@ -85,7 +156,7 @@ export const makePersistedAdapter =
|
|
|
85
156
|
Effect.forkScoped,
|
|
86
157
|
)
|
|
87
158
|
|
|
88
|
-
const devtoolsUrl = getDevtoolsUrl().toString()
|
|
159
|
+
const devtoolsUrl = devtoolsEnabled ? getDevtoolsUrl().toString() : 'ws://127.0.0.1:4242'
|
|
89
160
|
|
|
90
161
|
const { leaderThread, initialSnapshot } = yield* makeLeaderThread({
|
|
91
162
|
storeId,
|
|
@@ -96,7 +167,8 @@ export const makePersistedAdapter =
|
|
|
96
167
|
storage: storage ?? {},
|
|
97
168
|
devtoolsEnabled,
|
|
98
169
|
bootStatusQueue,
|
|
99
|
-
|
|
170
|
+
syncPayloadEncoded,
|
|
171
|
+
syncPayloadSchema,
|
|
100
172
|
devtoolsUrl,
|
|
101
173
|
})
|
|
102
174
|
|
|
@@ -128,10 +200,11 @@ export const makePersistedAdapter =
|
|
|
128
200
|
|
|
129
201
|
return () => {}
|
|
130
202
|
},
|
|
203
|
+
origin: undefined,
|
|
131
204
|
})
|
|
132
205
|
|
|
133
206
|
return clientSession
|
|
134
|
-
}).pipe(
|
|
207
|
+
}).pipe(UnknownError.mapToUnknownError, Effect.provide(FetchHttpClient.layer), Effect.tapCauseLogPretty)
|
|
135
208
|
|
|
136
209
|
const makeLeaderThread = ({
|
|
137
210
|
storeId,
|
|
@@ -142,7 +215,8 @@ const makeLeaderThread = ({
|
|
|
142
215
|
storage,
|
|
143
216
|
devtoolsEnabled,
|
|
144
217
|
bootStatusQueue: bootStatusQueueClientSession,
|
|
145
|
-
|
|
218
|
+
syncPayloadEncoded,
|
|
219
|
+
syncPayloadSchema,
|
|
146
220
|
devtoolsUrl,
|
|
147
221
|
}: {
|
|
148
222
|
storeId: string
|
|
@@ -155,21 +229,19 @@ const makeLeaderThread = ({
|
|
|
155
229
|
}
|
|
156
230
|
devtoolsEnabled: boolean
|
|
157
231
|
bootStatusQueue: Queue.Queue<BootStatus>
|
|
158
|
-
|
|
232
|
+
syncPayloadEncoded: Schema.JsonValue | undefined
|
|
233
|
+
syncPayloadSchema: Schema.Schema<any> | undefined
|
|
159
234
|
devtoolsUrl: string
|
|
160
235
|
}) =>
|
|
161
236
|
Effect.gen(function* () {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
|
|
168
|
-
const stateDatabaseName = `${'livestore-'}${schemaHashSuffix}@${liveStoreStorageFormatVersion}.db`
|
|
169
|
-
const dbEventlogPath = `${'livestore-'}eventlog@${liveStoreStorageFormatVersion}.db`
|
|
237
|
+
const { directory, stateDatabaseName, eventlogDatabaseName } = resolveExpoPersistencePaths({
|
|
238
|
+
storeId,
|
|
239
|
+
storage,
|
|
240
|
+
schema,
|
|
241
|
+
})
|
|
170
242
|
|
|
171
243
|
const dbState = yield* makeSqliteDb({ _tag: 'file', databaseName: stateDatabaseName, directory })
|
|
172
|
-
const dbEventlog = yield* makeSqliteDb({ _tag: 'file', databaseName:
|
|
244
|
+
const dbEventlog = yield* makeSqliteDb({ _tag: 'file', databaseName: eventlogDatabaseName, directory })
|
|
173
245
|
|
|
174
246
|
const devtoolsOptions = yield* makeDevtoolsOptions({
|
|
175
247
|
devtoolsEnabled,
|
|
@@ -192,7 +264,8 @@ const makeLeaderThread = ({
|
|
|
192
264
|
shutdownChannel: yield* makeShutdownChannel(storeId),
|
|
193
265
|
storeId,
|
|
194
266
|
syncOptions,
|
|
195
|
-
|
|
267
|
+
syncPayloadEncoded,
|
|
268
|
+
syncPayloadSchema,
|
|
196
269
|
}).pipe(Layer.provideMerge(FetchHttpClient.layer)),
|
|
197
270
|
)
|
|
198
271
|
|
|
@@ -204,6 +277,7 @@ const makeLeaderThread = ({
|
|
|
204
277
|
extraIncomingMessagesQueue,
|
|
205
278
|
initialState,
|
|
206
279
|
bootStatusQueue,
|
|
280
|
+
networkStatus,
|
|
207
281
|
} = yield* LeaderThreadCtx
|
|
208
282
|
|
|
209
283
|
const bootStatusFiber = yield* Queue.takeBetween(bootStatusQueue, 1, 1000).pipe(
|
|
@@ -227,16 +301,23 @@ const makeLeaderThread = ({
|
|
|
227
301
|
push: (batch) =>
|
|
228
302
|
syncProcessor
|
|
229
303
|
.push(
|
|
230
|
-
batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
|
|
304
|
+
batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)),
|
|
231
305
|
{ waitForProcessing: true },
|
|
232
306
|
)
|
|
233
307
|
.pipe(Effect.provide(layer), Effect.scoped),
|
|
308
|
+
stream: (options) =>
|
|
309
|
+
streamEventsWithSyncState({
|
|
310
|
+
dbEventlog,
|
|
311
|
+
syncState: syncProcessor.syncState,
|
|
312
|
+
options,
|
|
313
|
+
}),
|
|
234
314
|
},
|
|
235
315
|
initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
|
|
236
316
|
export: Effect.sync(() => db.export()),
|
|
237
317
|
getEventlogData: Effect.sync(() => dbEventlog.export()),
|
|
238
|
-
|
|
318
|
+
syncState: syncProcessor.syncState,
|
|
239
319
|
sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
|
|
320
|
+
networkStatus,
|
|
240
321
|
})
|
|
241
322
|
|
|
242
323
|
const initialSnapshot = db.export()
|
|
@@ -245,6 +326,57 @@ const makeLeaderThread = ({
|
|
|
245
326
|
}).pipe(Effect.provide(layer))
|
|
246
327
|
})
|
|
247
328
|
|
|
329
|
+
const resolveExpoPersistencePaths = ({
|
|
330
|
+
storeId,
|
|
331
|
+
storage,
|
|
332
|
+
schema,
|
|
333
|
+
}: {
|
|
334
|
+
storeId: string
|
|
335
|
+
storage: { subDirectory?: string } | undefined
|
|
336
|
+
schema: LiveStoreSchema
|
|
337
|
+
}) => {
|
|
338
|
+
const subDirectory = storage?.subDirectory ? `${storage.subDirectory.replace(/\/$/, '')}/` : ''
|
|
339
|
+
const pathJoin = (...paths: string[]) => paths.join('/').replaceAll(/\/+/g, '/')
|
|
340
|
+
const directory = pathJoin(SQLite.defaultDatabaseDirectory, subDirectory, storeId)
|
|
341
|
+
|
|
342
|
+
const schemaHashSuffix =
|
|
343
|
+
schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
|
|
344
|
+
const stateDatabaseName = `livestore-${schemaHashSuffix}@${liveStoreStorageFormatVersion}.db`
|
|
345
|
+
const eventlogDatabaseName = `livestore-eventlog@${liveStoreStorageFormatVersion}.db`
|
|
346
|
+
|
|
347
|
+
return { directory, stateDatabaseName, eventlogDatabaseName }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const resetExpoPersistence = ({
|
|
351
|
+
storeId,
|
|
352
|
+
storage,
|
|
353
|
+
schema,
|
|
354
|
+
}: {
|
|
355
|
+
storeId: string
|
|
356
|
+
storage: MakeDbOptions['storage']
|
|
357
|
+
schema: LiveStoreSchema
|
|
358
|
+
}) =>
|
|
359
|
+
Effect.try({
|
|
360
|
+
try: () => {
|
|
361
|
+
const { directory, stateDatabaseName, eventlogDatabaseName } = resolveExpoPersistencePaths({
|
|
362
|
+
storeId,
|
|
363
|
+
storage,
|
|
364
|
+
schema,
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
SQLite.deleteDatabaseSync(stateDatabaseName, directory)
|
|
368
|
+
SQLite.deleteDatabaseSync(eventlogDatabaseName, directory)
|
|
369
|
+
},
|
|
370
|
+
catch: (cause) =>
|
|
371
|
+
new UnknownError({
|
|
372
|
+
cause,
|
|
373
|
+
note: `@livestore/adapter-expo: Failed to reset persistence for store ${storeId}`,
|
|
374
|
+
}),
|
|
375
|
+
}).pipe(
|
|
376
|
+
Effect.retry({ schedule: Schedule.exponentialBackoff10Sec }),
|
|
377
|
+
Effect.withSpan('@livestore/adapter-expo:resetPersistence', { attributes: { storeId } }),
|
|
378
|
+
)
|
|
379
|
+
|
|
248
380
|
const makeDevtoolsOptions = ({
|
|
249
381
|
devtoolsEnabled,
|
|
250
382
|
devtoolsUrl,
|
|
@@ -259,7 +391,7 @@ const makeDevtoolsOptions = ({
|
|
|
259
391
|
dbEventlog: LeaderSqliteDb
|
|
260
392
|
storeId: string
|
|
261
393
|
clientId: string
|
|
262
|
-
}): Effect.Effect<DevtoolsOptions,
|
|
394
|
+
}): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope> =>
|
|
263
395
|
Effect.sync(() => {
|
|
264
396
|
if (devtoolsEnabled === false) {
|
|
265
397
|
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 {
|