@livestore/adapter-expo 0.3.0-dev.18 → 0.3.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 +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +133 -151
- package/dist/index.js.map +1 -1
- package/dist/make-sqlite-db.d.ts +20 -0
- package/dist/make-sqlite-db.d.ts.map +1 -0
- package/dist/make-sqlite-db.js +164 -0
- package/dist/make-sqlite-db.js.map +1 -0
- package/dist/shutdown-channel.d.ts +3 -0
- package/dist/shutdown-channel.d.ts.map +1 -0
- package/dist/shutdown-channel.js +8 -0
- package/dist/shutdown-channel.js.map +1 -0
- package/package.json +11 -6
- package/src/index.ts +253 -223
- package/src/make-sqlite-db.ts +201 -0
- package/src/shutdown-channel.ts +9 -0
- package/tsconfig.json +6 -1
- package/dist/common.d.ts +0 -13
- package/dist/common.d.ts.map +0 -1
- package/dist/common.js +0 -100
- package/dist/common.js.map +0 -1
- package/dist/devtools.d.ts +0 -22
- package/dist/devtools.d.ts.map +0 -1
- package/dist/devtools.js +0 -177
- package/dist/devtools.js.map +0 -1
- package/src/common.ts +0 -115
- package/src/devtools.ts +0 -292
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livestore/adapter-expo",
|
|
3
|
-
"version": "0.3.0-dev.
|
|
3
|
+
"version": "0.3.0-dev.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
@@ -12,22 +12,27 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@opentelemetry/api": "1.9.0",
|
|
15
|
-
"@livestore/common": "0.3.0-dev.
|
|
16
|
-
"@livestore/devtools-expo-common": "0.3.0-dev.
|
|
17
|
-
"@livestore/utils": "0.3.0-dev.
|
|
15
|
+
"@livestore/common": "0.3.0-dev.21",
|
|
16
|
+
"@livestore/devtools-expo-common": "0.3.0-dev.21",
|
|
17
|
+
"@livestore/utils": "0.3.0-dev.21",
|
|
18
|
+
"@livestore/webmesh": "0.3.0-dev.21"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"expo-file-system": "*",
|
|
21
|
-
"expo-sqlite": "^15.
|
|
22
|
+
"expo-sqlite": "^15.1.2"
|
|
22
23
|
},
|
|
23
24
|
"peerDependencies": {
|
|
24
25
|
"expo-file-system": "*",
|
|
25
|
-
"expo-sqlite": "~15.
|
|
26
|
+
"expo-sqlite": "~15.1.2"
|
|
26
27
|
},
|
|
27
28
|
"publishConfig": {
|
|
28
29
|
"access": "public"
|
|
29
30
|
},
|
|
31
|
+
"scripts_": {
|
|
32
|
+
"postinstall": "This is needed to avoid a Expo/PNPM related issue"
|
|
33
|
+
},
|
|
30
34
|
"scripts": {
|
|
35
|
+
"postinstall": "rm -f node_modules/expo-sqlite",
|
|
31
36
|
"test": "echo No tests yet"
|
|
32
37
|
}
|
|
33
38
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,251 +1,120 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
rehydrateFromMutationLog,
|
|
9
|
-
sql,
|
|
10
|
-
UnexpectedError,
|
|
1
|
+
import type {
|
|
2
|
+
Adapter,
|
|
3
|
+
BootStatus,
|
|
4
|
+
ClientSession,
|
|
5
|
+
ClientSessionLeaderThreadProxy,
|
|
6
|
+
LockStatus,
|
|
7
|
+
SyncOptions,
|
|
11
8
|
} from '@livestore/common'
|
|
12
|
-
import
|
|
13
|
-
import type {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from '@livestore/
|
|
21
|
-
import { insertRowPrepared, makeBindValues } from '@livestore/common/sql-queries'
|
|
22
|
-
import { casesHandled, shouldNeverHappen } from '@livestore/utils'
|
|
23
|
-
import { Effect, Option, Queue, Schema, Stream, SubscriptionRef } from '@livestore/utils/effect'
|
|
9
|
+
import { Devtools, liveStoreStorageFormatVersion, UnexpectedError } from '@livestore/common'
|
|
10
|
+
import type { DevtoolsOptions, LeaderSqliteDb } from '@livestore/common/leader-thread'
|
|
11
|
+
import { getClientHeadFromDb, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
|
|
12
|
+
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
13
|
+
import { MutationEvent } from '@livestore/common/schema'
|
|
14
|
+
import * as DevtoolsExpo from '@livestore/devtools-expo-common/web-channel'
|
|
15
|
+
import type { Scope } from '@livestore/utils/effect'
|
|
16
|
+
import { Cause, Effect, FetchHttpClient, Fiber, Layer, Queue, Stream, SubscriptionRef } from '@livestore/utils/effect'
|
|
17
|
+
import type { MeshNode } from '@livestore/webmesh'
|
|
24
18
|
import * as SQLite from 'expo-sqlite'
|
|
25
19
|
|
|
26
|
-
import {
|
|
27
|
-
import
|
|
28
|
-
import {
|
|
20
|
+
import type { MakeExpoSqliteDb } from './make-sqlite-db.js'
|
|
21
|
+
import { makeSqliteDb } from './make-sqlite-db.js'
|
|
22
|
+
import { makeShutdownChannel } from './shutdown-channel.js'
|
|
29
23
|
|
|
30
24
|
export type MakeDbOptions = {
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
sync?: SyncOptions
|
|
26
|
+
storage?: {
|
|
27
|
+
/**
|
|
28
|
+
* Relative to expo-sqlite's default directory
|
|
29
|
+
*
|
|
30
|
+
* Example of a resulting path for `subDirectory: 'my-app'`:
|
|
31
|
+
* `/data/Containers/Data/Application/<APP_UUID>/Documents/ExponentExperienceData/@<USERNAME>/<APPNAME>/SQLite/my-app/<STORE_ID>/livestore-mutationlog@3.db`
|
|
32
|
+
*/
|
|
33
|
+
subDirectory?: string
|
|
34
|
+
}
|
|
33
35
|
// syncBackend?: TODO
|
|
36
|
+
/** @default 'expo' */
|
|
37
|
+
clientId?: string
|
|
38
|
+
/** @default 'expo' */
|
|
39
|
+
sessionId?: string
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
// TODO refactor with leader-thread code from `@livestore/common/leader-thread`
|
|
37
43
|
export const makeAdapter =
|
|
38
|
-
(options
|
|
39
|
-
({ schema, connectDevtoolsToStore, shutdown, devtoolsEnabled }) =>
|
|
44
|
+
(options: MakeDbOptions = {}): Adapter =>
|
|
45
|
+
({ schema, connectDevtoolsToStore, shutdown, devtoolsEnabled, storeId, bootStatusQueue }) =>
|
|
40
46
|
Effect.gen(function* () {
|
|
41
|
-
const {
|
|
42
|
-
const migrationOptions = schema.migrationOptions
|
|
43
|
-
const subDirectoryPath = subDirectory ? subDirectory.replace(/\/$/, '') + '/' : ''
|
|
44
|
-
const fullDbFilePath = `${subDirectoryPath}${fileNamePrefix ?? 'livestore-'}${schema.hash}@${liveStoreStorageFormatVersion}.db`
|
|
45
|
-
const db = SQLite.openDatabaseSync(fullDbFilePath)
|
|
47
|
+
const { storage, clientId = 'expo', sessionId = 'expo', sync: syncOptions } = options
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
yield* Queue.offer(bootStatusQueue, { stage: 'loading' })
|
|
48
50
|
|
|
49
|
-
const
|
|
51
|
+
const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
|
|
50
52
|
|
|
51
|
-
const
|
|
52
|
-
`${subDirectory ?? ''}${fileNamePrefix ?? 'livestore-'}mutationlog@${liveStoreStorageFormatVersion}.db`,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
const dbMutationLogRef = { current: { db: dbMutationLog, sqliteDb: makeSqliteDb(dbMutationLog) } }
|
|
56
|
-
|
|
57
|
-
const dbMutationLogWasEmptyWhenOpened =
|
|
58
|
-
dbMutationLogRef.current.sqliteDb.select('SELECT 1 FROM sqlite_master').length === 0
|
|
59
|
-
|
|
60
|
-
yield* Effect.addFinalizer(() =>
|
|
61
|
-
Effect.gen(function* () {
|
|
62
|
-
// Ignoring in case the database is already closed
|
|
63
|
-
yield* Effect.try(() => db.closeSync()).pipe(Effect.ignore)
|
|
64
|
-
yield* Effect.try(() => dbMutationLog.closeSync()).pipe(Effect.ignore)
|
|
65
|
-
}),
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
if (dbMutationLogWasEmptyWhenOpened) {
|
|
69
|
-
yield* migrateTable({
|
|
70
|
-
db: dbMutationLogRef.current.sqliteDb,
|
|
71
|
-
behaviour: 'create-if-not-exists',
|
|
72
|
-
tableAst: mutationLogMetaTable.sqliteDef.ast,
|
|
73
|
-
skipMetaTable: true,
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (dbWasEmptyWhenOpened) {
|
|
78
|
-
yield* migrateDb({ db: dbRef.current.sqliteDb, schema })
|
|
79
|
-
|
|
80
|
-
initializeSingletonTables(schema, dbRef.current.sqliteDb)
|
|
81
|
-
|
|
82
|
-
switch (migrationOptions.strategy) {
|
|
83
|
-
case 'from-mutation-log': {
|
|
84
|
-
// TODO bring back
|
|
85
|
-
// yield* rehydrateFromMutationLog({
|
|
86
|
-
// db: dbRef.current.sqliteDb,
|
|
87
|
-
// logDb: dbMutationLogRef.current.sqliteDb,
|
|
88
|
-
// schema,
|
|
89
|
-
// migrationOptions,
|
|
90
|
-
// onProgress: () => Effect.void,
|
|
91
|
-
// })
|
|
92
|
-
|
|
93
|
-
break
|
|
94
|
-
}
|
|
95
|
-
case 'hard-reset': {
|
|
96
|
-
// This is already the case by note doing anything now
|
|
97
|
-
|
|
98
|
-
break
|
|
99
|
-
}
|
|
100
|
-
case 'manual': {
|
|
101
|
-
// const migrateFn = migrationStrategy.migrate
|
|
102
|
-
console.warn('Manual migration strategy not implemented yet')
|
|
103
|
-
|
|
104
|
-
// TODO figure out a way to get previous database file to pass to the migration function
|
|
105
|
-
|
|
106
|
-
break
|
|
107
|
-
}
|
|
108
|
-
default: {
|
|
109
|
-
casesHandled(migrationOptions)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const mutationLogExclude =
|
|
115
|
-
migrationOptions.strategy === 'from-mutation-log'
|
|
116
|
-
? (migrationOptions.excludeMutations ?? new Set(['livestore.RawSql']))
|
|
117
|
-
: new Set(['livestore.RawSql'])
|
|
118
|
-
|
|
119
|
-
const mutationEventSchema = MutationEvent.makeMutationEventSchema(schema)
|
|
120
|
-
const mutationDefSchemaHashMap = new Map(
|
|
121
|
-
[...schema.mutations.map.entries()].map(([k, v]) => [k, Schema.hash(v.schema)] as const),
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
const newMutationLogStmt = dbMutationLogRef.current.sqliteDb.prepare(
|
|
125
|
-
insertRowPrepared({ tableName: MUTATION_LOG_META_TABLE, columns: mutationLogMetaTable.sqliteDef.columns }),
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
const lockStatus = SubscriptionRef.make<LockStatus>('has-lock').pipe(Effect.runSync)
|
|
53
|
+
const shutdownChannel = yield* makeShutdownChannel(storeId)
|
|
129
54
|
|
|
130
|
-
|
|
131
|
-
|
|
55
|
+
yield* shutdownChannel.listen.pipe(
|
|
56
|
+
Stream.flatten(),
|
|
57
|
+
Stream.tap((error) => Effect.sync(() => shutdown(Cause.fail(error)))),
|
|
58
|
+
Stream.runDrain,
|
|
59
|
+
Effect.interruptible,
|
|
60
|
+
Effect.tapCauseLogPretty,
|
|
61
|
+
Effect.forkScoped,
|
|
132
62
|
)
|
|
133
63
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
64
|
+
const devtoolsWebmeshNode = devtoolsEnabled
|
|
65
|
+
? yield* DevtoolsExpo.makeExpoDevtoolsConnectedMeshNode({
|
|
66
|
+
nodeName: `expo-${storeId}-${clientId}-${sessionId}`,
|
|
67
|
+
target: `devtools-${storeId}-${clientId}-${sessionId}`,
|
|
68
|
+
})
|
|
69
|
+
: undefined
|
|
70
|
+
|
|
71
|
+
const { leaderThread, initialSnapshot } = yield* makeLeaderThread({
|
|
72
|
+
storeId,
|
|
73
|
+
clientId,
|
|
74
|
+
sessionId,
|
|
75
|
+
schema,
|
|
76
|
+
makeSqliteDb,
|
|
77
|
+
syncOptions,
|
|
78
|
+
storage: storage ?? {},
|
|
79
|
+
devtoolsEnabled,
|
|
80
|
+
devtoolsWebmeshNode,
|
|
81
|
+
bootStatusQueue,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const sqliteDb = yield* makeSqliteDb({ _tag: 'in-memory' })
|
|
85
|
+
sqliteDb.import(initialSnapshot)
|
|
152
86
|
|
|
153
87
|
const clientSession = {
|
|
154
88
|
devtools: { enabled: false },
|
|
155
89
|
lockStatus,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
leaderThread: {
|
|
160
|
-
mutations: {
|
|
161
|
-
pull: Stream.fromQueue(incomingSyncMutationsQueue),
|
|
162
|
-
push: (batch): Effect.Effect<void, UnexpectedError> =>
|
|
163
|
-
Effect.gen(function* () {
|
|
164
|
-
for (const mutationEventEncoded of batch) {
|
|
165
|
-
if (migrationOptions.strategy !== 'from-mutation-log') return
|
|
166
|
-
|
|
167
|
-
const mutation = mutationEventEncoded.mutation
|
|
168
|
-
const mutationDef = getMutationDef(schema, mutation)
|
|
169
|
-
|
|
170
|
-
const execArgsArr = getExecArgsFromMutation({
|
|
171
|
-
mutationDef,
|
|
172
|
-
mutationEvent: { decoded: undefined, encoded: mutationEventEncoded },
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
// write to mutation_log
|
|
176
|
-
if (
|
|
177
|
-
mutationLogExclude.has(mutation) === false &&
|
|
178
|
-
execArgsArr.some((_) => _.statementSql.includes('__livestore')) === false
|
|
179
|
-
) {
|
|
180
|
-
const mutationDefSchemaHash =
|
|
181
|
-
mutationDefSchemaHashMap.get(mutation) ?? shouldNeverHappen(`Unknown mutation: ${mutation}`)
|
|
182
|
-
|
|
183
|
-
const argsJson = JSON.stringify(mutationEventEncoded.args ?? {})
|
|
184
|
-
const mutationLogRowValues = {
|
|
185
|
-
idGlobal: mutationEventEncoded.id.global,
|
|
186
|
-
idClient: mutationEventEncoded.id.client,
|
|
187
|
-
mutation: mutationEventEncoded.mutation,
|
|
188
|
-
argsJson,
|
|
189
|
-
schemaHash: mutationDefSchemaHash,
|
|
190
|
-
syncMetadataJson: Option.none(),
|
|
191
|
-
parentIdGlobal: mutationEventEncoded.parentId.global,
|
|
192
|
-
parentIdClient: mutationEventEncoded.parentId.client,
|
|
193
|
-
clientId: 'expo',
|
|
194
|
-
sessionId: 'expo',
|
|
195
|
-
} satisfies MutationLogMetaRow
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
newMutationLogStmt.execute(
|
|
199
|
-
makeBindValues({
|
|
200
|
-
columns: mutationLogMetaTable.sqliteDef.columns,
|
|
201
|
-
values: mutationLogRowValues,
|
|
202
|
-
variablePrefix: '$',
|
|
203
|
-
}) as PreparedBindValues,
|
|
204
|
-
)
|
|
205
|
-
} catch (e) {
|
|
206
|
-
console.error('Error writing to mutation_log', e, mutationLogRowValues)
|
|
207
|
-
debugger
|
|
208
|
-
throw e
|
|
209
|
-
}
|
|
210
|
-
} else {
|
|
211
|
-
// console.debug('livestore-webworker: skipping mutation log write', mutation, statementSql, bindValues)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
yield* devtools?.onMutation({ mutationEventEncoded }) ?? Effect.void
|
|
215
|
-
}
|
|
216
|
-
}),
|
|
217
|
-
},
|
|
218
|
-
initialState: {
|
|
219
|
-
migrationsReport: {
|
|
220
|
-
migrations: [],
|
|
221
|
-
},
|
|
222
|
-
leaderHead: initialMutationEventId,
|
|
223
|
-
},
|
|
224
|
-
export: Effect.sync(() => dbRef.current.sqliteDb.export()),
|
|
225
|
-
getMutationLogData: Effect.sync(() => dbMutationLogRef.current.sqliteDb.export()),
|
|
226
|
-
networkStatus: SubscriptionRef.make({ isConnected: false, timestampMs: Date.now(), latchClosed: false }).pipe(
|
|
227
|
-
Effect.runSync,
|
|
228
|
-
),
|
|
229
|
-
sendDevtoolsMessage: () => Effect.dieMessage('Not implemented'),
|
|
230
|
-
getSyncState: Effect.dieMessage('Not implemented'),
|
|
231
|
-
},
|
|
90
|
+
clientId,
|
|
91
|
+
sessionId,
|
|
92
|
+
leaderThread,
|
|
232
93
|
shutdown: () => Effect.dieMessage('TODO implement shutdown'),
|
|
233
|
-
sqliteDb
|
|
94
|
+
sqliteDb,
|
|
234
95
|
} satisfies ClientSession
|
|
235
96
|
|
|
236
97
|
if (devtoolsEnabled) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
Effect.tapCauseLogPretty,
|
|
247
|
-
|
|
248
|
-
|
|
98
|
+
yield* Effect.gen(function* () {
|
|
99
|
+
const sessionInfoChannel = yield* DevtoolsExpo.makeExpoDevtoolsBroadcastChannel({
|
|
100
|
+
channelName: 'devtools-expo-session-info',
|
|
101
|
+
schema: Devtools.SessionInfo.Message,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
yield* Devtools.SessionInfo.provideSessionInfo({
|
|
105
|
+
webChannel: sessionInfoChannel,
|
|
106
|
+
sessionInfo: Devtools.SessionInfo.SessionInfo.make({ clientId, sessionId, storeId }),
|
|
107
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
|
108
|
+
|
|
109
|
+
const storeDevtoolsChannel = yield* DevtoolsExpo.makeChannelForConnectedMeshNode({
|
|
110
|
+
target: `devtools-${storeId}-${clientId}-${sessionId}`,
|
|
111
|
+
node: devtoolsWebmeshNode!,
|
|
112
|
+
schema: { listen: Devtools.ClientSession.MessageToApp, send: Devtools.ClientSession.MessageFromApp },
|
|
113
|
+
channelType: 'clientSession',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
yield* connectDevtoolsToStore(storeDevtoolsChannel)
|
|
117
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
|
249
118
|
}
|
|
250
119
|
|
|
251
120
|
return clientSession
|
|
@@ -253,3 +122,164 @@ export const makeAdapter =
|
|
|
253
122
|
Effect.mapError((cause) => (cause._tag === 'LiveStore.UnexpectedError' ? cause : new UnexpectedError({ cause }))),
|
|
254
123
|
Effect.tapCauseLogPretty,
|
|
255
124
|
)
|
|
125
|
+
|
|
126
|
+
const makeLeaderThread = ({
|
|
127
|
+
storeId,
|
|
128
|
+
clientId,
|
|
129
|
+
sessionId,
|
|
130
|
+
schema,
|
|
131
|
+
makeSqliteDb,
|
|
132
|
+
syncOptions,
|
|
133
|
+
storage,
|
|
134
|
+
devtoolsEnabled,
|
|
135
|
+
devtoolsWebmeshNode,
|
|
136
|
+
bootStatusQueue: bootStatusQueueClientSession,
|
|
137
|
+
}: {
|
|
138
|
+
storeId: string
|
|
139
|
+
clientId: string
|
|
140
|
+
sessionId: string
|
|
141
|
+
schema: LiveStoreSchema
|
|
142
|
+
makeSqliteDb: MakeExpoSqliteDb
|
|
143
|
+
syncOptions: SyncOptions | undefined
|
|
144
|
+
storage: {
|
|
145
|
+
subDirectory?: string
|
|
146
|
+
}
|
|
147
|
+
devtoolsEnabled: boolean
|
|
148
|
+
devtoolsWebmeshNode: MeshNode | undefined
|
|
149
|
+
bootStatusQueue: Queue.Queue<BootStatus>
|
|
150
|
+
}) =>
|
|
151
|
+
Effect.gen(function* () {
|
|
152
|
+
const subDirectory = storage.subDirectory ? storage.subDirectory.replace(/\/$/, '') + '/' : ''
|
|
153
|
+
const pathJoin = (...paths: string[]) => paths.join('/').replaceAll(/\/+/g, '/')
|
|
154
|
+
const directory = pathJoin(SQLite.defaultDatabaseDirectory, subDirectory, storeId)
|
|
155
|
+
|
|
156
|
+
const readModelDatabaseName = `${'livestore-'}${schema.hash}@${liveStoreStorageFormatVersion}.db`
|
|
157
|
+
const dbMutationLogPath = `${'livestore-'}mutationlog@${liveStoreStorageFormatVersion}.db`
|
|
158
|
+
|
|
159
|
+
const dbReadModel = yield* makeSqliteDb({ _tag: 'expo', databaseName: readModelDatabaseName, directory })
|
|
160
|
+
const dbMutationLog = yield* makeSqliteDb({ _tag: 'expo', databaseName: dbMutationLogPath, directory })
|
|
161
|
+
|
|
162
|
+
const devtoolsOptions = yield* makeDevtoolsOptions({
|
|
163
|
+
devtoolsEnabled,
|
|
164
|
+
dbReadModel,
|
|
165
|
+
dbMutationLog,
|
|
166
|
+
storeId,
|
|
167
|
+
clientId,
|
|
168
|
+
sessionId,
|
|
169
|
+
devtoolsWebmeshNode,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const layer = yield* Layer.memoize(
|
|
173
|
+
makeLeaderThreadLayer({
|
|
174
|
+
clientId,
|
|
175
|
+
dbReadModel,
|
|
176
|
+
dbMutationLog,
|
|
177
|
+
devtoolsOptions,
|
|
178
|
+
makeSqliteDb,
|
|
179
|
+
schema,
|
|
180
|
+
// NOTE we're creating a separate channel here since you can't listen to your own channel messages
|
|
181
|
+
shutdownChannel: yield* makeShutdownChannel(storeId),
|
|
182
|
+
storeId,
|
|
183
|
+
syncOptions,
|
|
184
|
+
}).pipe(Layer.provideMerge(FetchHttpClient.layer)),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return yield* Effect.gen(function* () {
|
|
188
|
+
const {
|
|
189
|
+
dbReadModel: db,
|
|
190
|
+
dbMutationLog,
|
|
191
|
+
syncProcessor,
|
|
192
|
+
connectedClientSessionPullQueues,
|
|
193
|
+
extraIncomingMessagesQueue,
|
|
194
|
+
initialState,
|
|
195
|
+
bootStatusQueue,
|
|
196
|
+
} = yield* LeaderThreadCtx
|
|
197
|
+
|
|
198
|
+
const bootStatusFiber = yield* Queue.takeBetween(bootStatusQueue, 1, 1000).pipe(
|
|
199
|
+
Effect.tap((bootStatus) => Queue.offerAll(bootStatusQueueClientSession, bootStatus)),
|
|
200
|
+
Effect.interruptible,
|
|
201
|
+
Effect.tapCauseLogPretty,
|
|
202
|
+
Effect.forkScoped,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
yield* Queue.awaitShutdown(bootStatusQueueClientSession).pipe(
|
|
206
|
+
Effect.andThen(Fiber.interrupt(bootStatusFiber)),
|
|
207
|
+
Effect.tapCauseLogPretty,
|
|
208
|
+
Effect.forkScoped,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
const initialLeaderHead = getClientHeadFromDb(dbMutationLog)
|
|
212
|
+
const pullQueue = yield* connectedClientSessionPullQueues.makeQueue(initialLeaderHead)
|
|
213
|
+
|
|
214
|
+
const leaderThread = {
|
|
215
|
+
mutations: {
|
|
216
|
+
pull: Stream.fromQueue(pullQueue),
|
|
217
|
+
push: (batch) =>
|
|
218
|
+
syncProcessor
|
|
219
|
+
.push(
|
|
220
|
+
batch.map((item) => new MutationEvent.EncodedWithMeta(item)),
|
|
221
|
+
{ waitForProcessing: true },
|
|
222
|
+
)
|
|
223
|
+
.pipe(Effect.provide(layer), Effect.scoped),
|
|
224
|
+
},
|
|
225
|
+
initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
|
|
226
|
+
export: Effect.sync(() => db.export()),
|
|
227
|
+
getMutationLogData: Effect.sync(() => dbMutationLog.export()),
|
|
228
|
+
// TODO
|
|
229
|
+
networkStatus: SubscriptionRef.make({ isConnected: false, timestampMs: Date.now(), latchClosed: false }).pipe(
|
|
230
|
+
Effect.runSync,
|
|
231
|
+
),
|
|
232
|
+
getSyncState: syncProcessor.syncState,
|
|
233
|
+
sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
|
|
234
|
+
} satisfies ClientSessionLeaderThreadProxy
|
|
235
|
+
|
|
236
|
+
const initialSnapshot = db.export()
|
|
237
|
+
|
|
238
|
+
return { leaderThread, initialSnapshot }
|
|
239
|
+
}).pipe(Effect.provide(layer))
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const makeDevtoolsOptions = ({
|
|
243
|
+
devtoolsEnabled,
|
|
244
|
+
dbReadModel,
|
|
245
|
+
dbMutationLog,
|
|
246
|
+
storeId,
|
|
247
|
+
clientId,
|
|
248
|
+
sessionId,
|
|
249
|
+
devtoolsWebmeshNode,
|
|
250
|
+
}: {
|
|
251
|
+
devtoolsEnabled: boolean
|
|
252
|
+
dbReadModel: LeaderSqliteDb
|
|
253
|
+
dbMutationLog: LeaderSqliteDb
|
|
254
|
+
storeId: string
|
|
255
|
+
clientId: string
|
|
256
|
+
sessionId: string
|
|
257
|
+
devtoolsWebmeshNode: MeshNode | undefined
|
|
258
|
+
}): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
|
|
259
|
+
Effect.gen(function* () {
|
|
260
|
+
if (devtoolsEnabled === false) {
|
|
261
|
+
return {
|
|
262
|
+
enabled: false,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
enabled: true,
|
|
268
|
+
makeBootContext: Effect.gen(function* () {
|
|
269
|
+
const devtoolsWebChannel = yield* DevtoolsExpo.makeChannelForConnectedMeshNode({
|
|
270
|
+
node: devtoolsWebmeshNode!,
|
|
271
|
+
target: `devtools-${storeId}-${clientId}-${sessionId}`,
|
|
272
|
+
schema: { listen: Devtools.Leader.MessageToApp, send: Devtools.Leader.MessageFromApp },
|
|
273
|
+
channelType: 'leader',
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
devtoolsWebChannel,
|
|
278
|
+
persistenceInfo: {
|
|
279
|
+
readModel: dbReadModel.metadata.persistenceInfo,
|
|
280
|
+
mutationLog: dbMutationLog.metadata.persistenceInfo,
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
}),
|
|
284
|
+
}
|
|
285
|
+
})
|