@livestore/livestore 0.3.0-dev.5 → 0.3.0-dev.50
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/QueryCache.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.d.ts +60 -0
- package/dist/SqliteDbWrapper.d.ts.map +1 -0
- package/dist/{SynchronousDatabaseWrapper.js → SqliteDbWrapper.js} +69 -34
- package/dist/SqliteDbWrapper.js.map +1 -0
- package/dist/effect/LiveStore.d.ts +6 -34
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +10 -12
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/mod.d.ts +3 -0
- package/dist/effect/mod.d.ts.map +1 -0
- package/dist/effect/mod.js +3 -0
- package/dist/effect/mod.js.map +1 -0
- package/dist/internal/mod.d.ts +3 -0
- package/dist/internal/mod.d.ts.map +1 -0
- package/dist/internal/mod.js +3 -0
- package/dist/internal/mod.js.map +1 -0
- package/dist/live-queries/base-class.d.ts +65 -27
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +54 -13
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/client-document-get-query.d.ts +12 -0
- package/dist/live-queries/client-document-get-query.d.ts.map +1 -0
- package/dist/live-queries/client-document-get-query.js +18 -0
- package/dist/live-queries/client-document-get-query.js.map +1 -0
- package/dist/live-queries/computed.d.ts +12 -14
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +37 -15
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.d.ts +93 -0
- package/dist/live-queries/db-query.d.ts.map +1 -0
- package/dist/live-queries/{db.js → db-query.js} +111 -40
- package/dist/live-queries/db-query.js.map +1 -0
- package/dist/live-queries/db-query.test.d.ts +2 -0
- package/dist/live-queries/db-query.test.d.ts.map +1 -0
- package/dist/live-queries/db-query.test.js +133 -0
- package/dist/live-queries/db-query.test.js.map +1 -0
- package/dist/live-queries/mod.d.ts +5 -0
- package/dist/live-queries/mod.d.ts.map +1 -0
- package/dist/live-queries/mod.js +5 -0
- package/dist/live-queries/mod.js.map +1 -0
- package/dist/live-queries/signal.d.ts +20 -0
- package/dist/live-queries/signal.d.ts.map +1 -0
- package/dist/live-queries/signal.js +33 -0
- package/dist/live-queries/signal.js.map +1 -0
- package/dist/live-queries/signal.test.d.ts +2 -0
- package/dist/live-queries/signal.test.d.ts.map +1 -0
- package/dist/live-queries/signal.test.js +25 -0
- package/dist/live-queries/signal.test.js.map +1 -0
- package/dist/mod.d.ts +14 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +13 -0
- package/dist/mod.js.map +1 -0
- package/dist/reactive.d.ts +23 -17
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +23 -19
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +1 -1
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/create-store.d.ts +70 -12
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +68 -19
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +5 -4
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +92 -40
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-types.d.ts +54 -42
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js +2 -5
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store.d.ts +141 -35
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +319 -153
- package/dist/store/store.js.map +1 -1
- package/dist/utils/data-structures.d.ts.map +1 -1
- package/dist/utils/dev.d.ts.map +1 -1
- package/dist/utils/dev.js +6 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/function-string.d.ts +7 -0
- package/dist/utils/function-string.d.ts.map +1 -0
- package/dist/utils/function-string.js +9 -0
- package/dist/utils/function-string.js.map +1 -0
- package/dist/utils/stack-info.d.ts.map +1 -1
- package/dist/utils/stack-info.js +6 -1
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/stack-info.test.js +54 -1
- package/dist/utils/stack-info.test.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +59 -216
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js +23 -18
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/mod.d.ts +1 -0
- package/dist/utils/tests/mod.d.ts.map +1 -1
- package/dist/utils/tests/mod.js +1 -0
- package/dist/utils/tests/mod.js.map +1 -1
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +8 -3
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +29 -26
- package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +92 -42
- package/src/effect/LiveStore.ts +27 -64
- package/src/effect/{index.ts → mod.ts} +2 -3
- package/src/internal/mod.ts +2 -0
- package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +241 -45
- package/src/live-queries/base-class.ts +152 -50
- package/src/live-queries/client-document-get-query.ts +52 -0
- package/src/live-queries/computed.ts +51 -33
- package/src/live-queries/db-query.test.ts +192 -0
- package/src/live-queries/{db.ts → db-query.ts} +168 -81
- package/src/live-queries/mod.ts +4 -0
- package/src/live-queries/signal.test.ts +40 -0
- package/src/live-queries/signal.ts +47 -0
- package/src/mod.ts +42 -0
- package/src/reactive.test.ts +1 -1
- package/src/reactive.ts +66 -43
- package/src/store/create-store.ts +188 -62
- package/src/store/devtools.ts +124 -46
- package/src/store/store-types.ts +54 -43
- package/src/store/store.ts +454 -236
- package/src/utils/dev.ts +6 -1
- package/src/utils/function-string.ts +12 -0
- package/src/utils/stack-info.test.ts +58 -1
- package/src/utils/stack-info.ts +6 -1
- package/src/utils/tests/fixture.ts +22 -31
- package/src/utils/tests/mod.ts +1 -0
- package/src/utils/tests/otel.ts +10 -3
- package/dist/SynchronousDatabaseWrapper.d.ts +0 -41
- package/dist/SynchronousDatabaseWrapper.d.ts.map +0 -1
- package/dist/SynchronousDatabaseWrapper.js.map +0 -1
- package/dist/effect/index.d.ts +0 -2
- package/dist/effect/index.d.ts.map +0 -1
- package/dist/effect/index.js +0 -2
- package/dist/effect/index.js.map +0 -1
- package/dist/global-state.d.ts +0 -14
- package/dist/global-state.d.ts.map +0 -1
- package/dist/global-state.js +0 -16
- package/dist/global-state.js.map +0 -1
- package/dist/index.d.ts +0 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -16
- package/dist/index.js.map +0 -1
- package/dist/live-queries/db.d.ts +0 -66
- package/dist/live-queries/db.d.ts.map +0 -1
- package/dist/live-queries/db.js.map +0 -1
- package/dist/live-queries/db.test.d.ts +0 -2
- package/dist/live-queries/db.test.d.ts.map +0 -1
- package/dist/live-queries/db.test.js +0 -118
- package/dist/live-queries/db.test.js.map +0 -1
- package/dist/live-queries/graphql.d.ts +0 -49
- package/dist/live-queries/graphql.d.ts.map +0 -1
- package/dist/live-queries/graphql.js +0 -122
- package/dist/live-queries/graphql.js.map +0 -1
- package/dist/row-query-utils.d.ts +0 -17
- package/dist/row-query-utils.d.ts.map +0 -1
- package/dist/row-query-utils.js +0 -30
- package/dist/row-query-utils.js.map +0 -1
- package/dist/utils/otel.d.ts +0 -4
- package/dist/utils/otel.d.ts.map +0 -1
- package/dist/utils/otel.js +0 -6
- package/dist/utils/otel.js.map +0 -1
- package/src/global-state.ts +0 -20
- package/src/index.ts +0 -66
- package/src/live-queries/db.test.ts +0 -154
- package/src/live-queries/graphql.ts +0 -219
- package/src/row-query-utils.ts +0 -65
- package/src/utils/otel.ts +0 -9
- package/tsconfig.json +0 -18
- package/vitest.config.js +0 -9
@@ -2,21 +2,24 @@ import type {
|
|
2
2
|
Adapter,
|
3
3
|
BootStatus,
|
4
4
|
ClientSession,
|
5
|
+
ClientSessionDevtoolsChannel,
|
5
6
|
IntentionalShutdownCause,
|
6
|
-
|
7
|
+
MigrationsReport,
|
7
8
|
} from '@livestore/common'
|
8
9
|
import { provideOtel, UnexpectedError } from '@livestore/common'
|
9
|
-
import type {
|
10
|
-
import { LS_DEV } from '@livestore/utils'
|
11
|
-
import type { Cause } from '@livestore/utils/effect'
|
10
|
+
import type { LiveStoreSchema } from '@livestore/common/schema'
|
11
|
+
import { isDevEnv, LS_DEV } from '@livestore/utils'
|
12
|
+
import type { Cause, Schema } from '@livestore/utils/effect'
|
12
13
|
import {
|
14
|
+
Context,
|
13
15
|
Deferred,
|
14
16
|
Effect,
|
15
17
|
Exit,
|
18
|
+
Fiber,
|
16
19
|
identity,
|
20
|
+
Layer,
|
17
21
|
Logger,
|
18
22
|
LogLevel,
|
19
|
-
MutableHashMap,
|
20
23
|
OtelTracer,
|
21
24
|
Queue,
|
22
25
|
Runtime,
|
@@ -26,43 +29,112 @@ import {
|
|
26
29
|
import { nanoid } from '@livestore/utils/nanoid'
|
27
30
|
import * as otel from '@opentelemetry/api'
|
28
31
|
|
29
|
-
import { globalReactivityGraph } from '../global-state.js'
|
30
|
-
import type { ReactivityGraph } from '../live-queries/base-class.js'
|
31
32
|
import { connectDevtoolsToStore } from './devtools.js'
|
32
33
|
import { Store } from './store.js'
|
33
|
-
import type {
|
34
|
+
import type {
|
35
|
+
LiveStoreContextRunning as LiveStoreContextRunning_,
|
36
|
+
OtelOptions,
|
37
|
+
ShutdownDeferred,
|
38
|
+
} from './store-types.js'
|
39
|
+
|
40
|
+
export const DEFAULT_PARAMS = {
|
41
|
+
leaderPushBatchSize: 1,
|
42
|
+
}
|
43
|
+
|
44
|
+
export class LiveStoreContextRunning extends Context.Tag('@livestore/livestore/effect/LiveStoreContextRunning')<
|
45
|
+
LiveStoreContextRunning,
|
46
|
+
LiveStoreContextRunning_
|
47
|
+
>() {
|
48
|
+
static fromDeferred = Effect.gen(function* () {
|
49
|
+
const deferred = yield* DeferredStoreContext
|
50
|
+
const ctx = yield* deferred
|
51
|
+
return Layer.succeed(LiveStoreContextRunning, ctx)
|
52
|
+
}).pipe(Layer.unwrapScoped)
|
53
|
+
}
|
54
|
+
|
55
|
+
export class DeferredStoreContext extends Context.Tag('@livestore/livestore/effect/DeferredStoreContext')<
|
56
|
+
DeferredStoreContext,
|
57
|
+
Deferred.Deferred<LiveStoreContextRunning['Type'], UnexpectedError>
|
58
|
+
>() {}
|
59
|
+
|
60
|
+
export type LiveStoreContextProps<TSchema extends LiveStoreSchema, TContext = {}> = {
|
61
|
+
schema: TSchema
|
62
|
+
/**
|
63
|
+
* The `storeId` can be used to isolate multiple stores from each other.
|
64
|
+
* So it can be useful for multi-tenancy scenarios.
|
65
|
+
*
|
66
|
+
* The `storeId` is also used for persistence.
|
67
|
+
*
|
68
|
+
* @default 'default'
|
69
|
+
*/
|
70
|
+
storeId?: string
|
71
|
+
/** Can be useful for custom live query implementations (e.g. see `@livestore/graphql`) */
|
72
|
+
context?: TContext
|
73
|
+
boot?: (
|
74
|
+
store: Store<TSchema, TContext>,
|
75
|
+
) => Effect.Effect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
|
76
|
+
adapter: Adapter
|
77
|
+
/**
|
78
|
+
* Whether to disable devtools.
|
79
|
+
*
|
80
|
+
* @default 'auto'
|
81
|
+
*/
|
82
|
+
disableDevtools?: boolean | 'auto'
|
83
|
+
onBootStatus?: (status: BootStatus) => void
|
84
|
+
batchUpdates: (run: () => void) => void
|
85
|
+
}
|
34
86
|
|
35
|
-
export interface CreateStoreOptions<
|
87
|
+
export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext = {}> {
|
36
88
|
schema: TSchema
|
37
89
|
adapter: Adapter
|
38
90
|
storeId: string
|
39
|
-
|
40
|
-
graphQLOptions?: GraphQLOptions<TGraphQLContext>
|
91
|
+
context?: TContext
|
41
92
|
boot?: (
|
42
|
-
store: Store<
|
43
|
-
|
44
|
-
|
93
|
+
store: Store<TSchema, TContext>,
|
94
|
+
ctx: {
|
95
|
+
migrationsReport: MigrationsReport
|
96
|
+
parentSpan: otel.Span
|
97
|
+
},
|
98
|
+
) => void | Promise<void> | Effect.Effect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
|
45
99
|
batchUpdates?: (run: () => void) => void
|
46
|
-
|
100
|
+
/**
|
101
|
+
* Whether to disable devtools.
|
102
|
+
*
|
103
|
+
* @default 'auto'
|
104
|
+
*/
|
105
|
+
disableDevtools?: boolean | 'auto'
|
47
106
|
onBootStatus?: (status: BootStatus) => void
|
48
107
|
shutdownDeferred?: ShutdownDeferred
|
108
|
+
/**
|
109
|
+
* Currently only used in the web adapter:
|
110
|
+
* If true, registers a beforeunload event listener to confirm unsaved changes.
|
111
|
+
*
|
112
|
+
* @default true
|
113
|
+
*/
|
114
|
+
confirmUnsavedChanges?: boolean
|
115
|
+
/**
|
116
|
+
* Payload that will be passed to the sync backend when connecting
|
117
|
+
*
|
118
|
+
* @default undefined
|
119
|
+
*/
|
120
|
+
syncPayload?: Schema.JsonValue
|
121
|
+
params?: {
|
122
|
+
leaderPushBatchSize?: number
|
123
|
+
}
|
49
124
|
debug?: {
|
50
125
|
instanceId?: string
|
51
126
|
}
|
52
127
|
}
|
53
128
|
|
54
129
|
/** Create a new LiveStore Store */
|
55
|
-
export const createStorePromise = async <
|
56
|
-
TGraphQLContext extends BaseGraphQLContext,
|
57
|
-
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
58
|
-
>({
|
130
|
+
export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}>({
|
59
131
|
signal,
|
60
132
|
otelOptions,
|
61
133
|
...options
|
62
|
-
}: CreateStoreOptions<
|
134
|
+
}: CreateStoreOptions<TSchema, TContext> & {
|
63
135
|
signal?: AbortSignal
|
64
136
|
otelOptions?: Partial<OtelOptions>
|
65
|
-
}): Promise<Store<
|
137
|
+
}): Promise<Store<TSchema, TContext>> =>
|
66
138
|
Effect.gen(function* () {
|
67
139
|
const scope = yield* Scope.make()
|
68
140
|
const runtime = yield* Effect.runtime()
|
@@ -81,34 +153,35 @@ export const createStorePromise = async <
|
|
81
153
|
provideOtel({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer }),
|
82
154
|
Effect.tapCauseLogPretty,
|
83
155
|
Effect.annotateLogs({ thread: 'window' }),
|
84
|
-
Effect.provide(Logger.
|
156
|
+
Effect.provide(Logger.prettyWithThread('window')),
|
85
157
|
Logger.withMinimumLogLevel(LogLevel.Debug),
|
86
158
|
Effect.runPromise,
|
87
159
|
)
|
88
160
|
|
89
|
-
export const createStore = <
|
90
|
-
TGraphQLContext extends BaseGraphQLContext,
|
91
|
-
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
92
|
-
>({
|
161
|
+
export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}>({
|
93
162
|
schema,
|
94
163
|
adapter,
|
95
164
|
storeId,
|
96
|
-
|
165
|
+
context = {} as TContext,
|
97
166
|
boot,
|
98
|
-
reactivityGraph = globalReactivityGraph,
|
99
167
|
batchUpdates,
|
100
168
|
disableDevtools,
|
101
169
|
onBootStatus,
|
102
170
|
shutdownDeferred,
|
171
|
+
params,
|
103
172
|
debug,
|
104
|
-
|
105
|
-
|
173
|
+
confirmUnsavedChanges = true,
|
174
|
+
syncPayload,
|
175
|
+
}: CreateStoreOptions<TSchema, TContext>): Effect.Effect<
|
176
|
+
Store<TSchema, TContext>,
|
106
177
|
UnexpectedError,
|
107
178
|
Scope.Scope | OtelTracer.OtelTracer
|
108
179
|
> =>
|
109
180
|
Effect.gen(function* () {
|
110
181
|
const lifetimeScope = yield* Scope.make()
|
111
182
|
|
183
|
+
yield* validateStoreId(storeId)
|
184
|
+
|
112
185
|
yield* Effect.addFinalizer((_) => Scope.close(lifetimeScope, _))
|
113
186
|
|
114
187
|
const debugInstanceId = debug?.instanceId ?? nanoid(10)
|
@@ -130,7 +203,7 @@ export const createStore = <
|
|
130
203
|
|
131
204
|
const storeDeferred = yield* Deferred.make<Store>()
|
132
205
|
|
133
|
-
const connectDevtoolsToStore_ = (storeDevtoolsChannel:
|
206
|
+
const connectDevtoolsToStore_ = (storeDevtoolsChannel: ClientSessionDevtoolsChannel) =>
|
134
207
|
Effect.gen(function* () {
|
135
208
|
const store = yield* storeDeferred
|
136
209
|
yield* connectDevtoolsToStore({ storeDevtoolsChannel, store })
|
@@ -139,53 +212,81 @@ export const createStore = <
|
|
139
212
|
const runtime = yield* Effect.runtime<Scope.Scope>()
|
140
213
|
|
141
214
|
const shutdown = (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) =>
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
Effect.
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
215
|
+
Effect.gen(function* () {
|
216
|
+
yield* Scope.close(lifetimeScope, Exit.failCause(cause)).pipe(
|
217
|
+
Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
|
218
|
+
Effect.timeout(1000),
|
219
|
+
Effect.catchTag('TimeoutException', () =>
|
220
|
+
Effect.logError('@livestore/livestore:shutdown: Timed out after 1 second'),
|
221
|
+
),
|
222
|
+
)
|
223
|
+
|
224
|
+
if (shutdownDeferred) {
|
225
|
+
yield* Deferred.failCause(shutdownDeferred, cause)
|
226
|
+
}
|
227
|
+
|
228
|
+
yield* Effect.logDebug('LiveStore shutdown complete')
|
229
|
+
}).pipe(
|
230
|
+
Effect.withSpan('@livestore/livestore:shutdown'),
|
231
|
+
Effect.provide(runtime),
|
232
|
+
Effect.tapCauseLogPretty,
|
233
|
+
// Given that the shutdown flow might also interrupt the effect that is calling the shutdown,
|
234
|
+
// we want to detach the shutdown effect so it's not interrupted by itself
|
235
|
+
Effect.runFork,
|
236
|
+
Fiber.join,
|
151
237
|
)
|
152
238
|
|
153
239
|
const clientSession: ClientSession = yield* adapter({
|
154
240
|
schema,
|
155
241
|
storeId,
|
156
|
-
devtoolsEnabled: disableDevtools
|
242
|
+
devtoolsEnabled: getDevtoolsEnabled(disableDevtools),
|
157
243
|
bootStatusQueue,
|
158
244
|
shutdown,
|
159
245
|
connectDevtoolsToStore: connectDevtoolsToStore_,
|
160
246
|
debugInstanceId,
|
247
|
+
syncPayload,
|
161
248
|
}).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
|
162
249
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
250
|
+
if (LS_DEV && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
|
251
|
+
yield* Effect.logDebug(
|
252
|
+
'[@livestore/livestore:createStore] migrationsReport',
|
253
|
+
...clientSession.leaderThread.initialState.migrationsReport.migrations.map((m) =>
|
254
|
+
m.hashes.actual === undefined
|
255
|
+
? `Table '${m.tableName}' doesn't exist yet. Creating table...`
|
256
|
+
: `Schema hash mismatch for table '${m.tableName}' (DB: ${m.hashes.actual}, expected: ${m.hashes.expected}), migrating table...`,
|
257
|
+
),
|
258
|
+
)
|
259
|
+
}
|
260
|
+
|
261
|
+
const store = new Store<TSchema, TContext>({
|
262
|
+
clientSession,
|
263
|
+
schema,
|
264
|
+
context,
|
265
|
+
otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
|
266
|
+
effectContext: { lifetimeScope, runtime },
|
267
|
+
// TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
|
268
|
+
// But for now this is a good enough approximation with little downsides
|
269
|
+
__runningInDevtools: getDevtoolsEnabled(disableDevtools) === false,
|
270
|
+
confirmUnsavedChanges,
|
271
|
+
// NOTE during boot we're not yet executing events in a batched context
|
272
|
+
// but only set the provided `batchUpdates` function after boot
|
273
|
+
batchUpdates: (run) => run(),
|
274
|
+
storeId,
|
275
|
+
params: {
|
276
|
+
leaderPushBatchSize: params?.leaderPushBatchSize ?? DEFAULT_PARAMS.leaderPushBatchSize,
|
181
277
|
},
|
182
|
-
|
183
|
-
|
278
|
+
})
|
279
|
+
|
280
|
+
// Starts background fibers (syncing, event processing, etc) for store
|
281
|
+
yield* store.boot
|
184
282
|
|
185
283
|
if (boot !== undefined) {
|
186
284
|
// TODO also incorporate `boot` function progress into `bootStatusQueue`
|
187
|
-
yield* Effect.tryAll(() =>
|
285
|
+
yield* Effect.tryAll(() =>
|
286
|
+
boot(store, { migrationsReport: clientSession.leaderThread.initialState.migrationsReport, parentSpan: span }),
|
287
|
+
).pipe(
|
188
288
|
UnexpectedError.mapToUnexpectedError,
|
289
|
+
Effect.provide(Layer.succeed(LiveStoreContextRunning, { stage: 'running', store: store as any as Store })),
|
189
290
|
Effect.withSpan('createStore:boot'),
|
190
291
|
)
|
191
292
|
}
|
@@ -208,3 +309,28 @@ export const createStore = <
|
|
208
309
|
Scope.extend(lifetimeScope),
|
209
310
|
)
|
210
311
|
})
|
312
|
+
|
313
|
+
const validateStoreId = (storeId: string) =>
|
314
|
+
Effect.gen(function* () {
|
315
|
+
const validChars = /^[a-zA-Z0-9_-]+$/
|
316
|
+
|
317
|
+
if (!validChars.test(storeId)) {
|
318
|
+
return yield* UnexpectedError.make({
|
319
|
+
cause: `Invalid storeId: ${storeId}. Only alphanumeric characters, underscores, and hyphens are allowed.`,
|
320
|
+
payload: { storeId },
|
321
|
+
})
|
322
|
+
}
|
323
|
+
})
|
324
|
+
|
325
|
+
const getDevtoolsEnabled = (disableDevtools: boolean | 'auto' | undefined) => {
|
326
|
+
// If an explicit value is provided, use that
|
327
|
+
if (disableDevtools === true || disableDevtools === false) {
|
328
|
+
return !disableDevtools
|
329
|
+
}
|
330
|
+
|
331
|
+
if (isDevEnv() === true) {
|
332
|
+
return true
|
333
|
+
}
|
334
|
+
|
335
|
+
return false
|
336
|
+
}
|
package/src/store/devtools.ts
CHANGED
@@ -1,20 +1,22 @@
|
|
1
|
-
import type { ClientSession, DebugInfo } from '@livestore/common'
|
1
|
+
import type { ClientSession, ClientSessionSyncProcessor, DebugInfo, SyncState } from '@livestore/common'
|
2
2
|
import { Devtools, liveStoreVersion, UnexpectedError } from '@livestore/common'
|
3
3
|
import { throttle } from '@livestore/utils'
|
4
4
|
import type { WebChannel } from '@livestore/utils/effect'
|
5
5
|
import { Effect, Stream } from '@livestore/utils/effect'
|
6
|
+
import { nanoid } from '@livestore/utils/nanoid'
|
6
7
|
|
7
8
|
import type { LiveQuery, ReactivityGraph } from '../live-queries/base-class.js'
|
8
9
|
import { NOT_REFRESHED_YET } from '../reactive.js'
|
9
|
-
import type {
|
10
|
-
import { emptyDebugInfo as makeEmptyDebugInfo } from '../
|
10
|
+
import type { SqliteDbWrapper } from '../SqliteDbWrapper.js'
|
11
|
+
import { emptyDebugInfo as makeEmptyDebugInfo } from '../SqliteDbWrapper.js'
|
11
12
|
import type { ReferenceCountedSet } from '../utils/data-structures.js'
|
12
13
|
|
13
14
|
type IStore = {
|
14
15
|
clientSession: ClientSession
|
15
16
|
reactivityGraph: ReactivityGraph
|
16
|
-
|
17
|
+
sqliteDbWrapper: SqliteDbWrapper
|
17
18
|
activeQueries: ReferenceCountedSet<LiveQuery<any>>
|
19
|
+
syncProcessor: ClientSessionSyncProcessor
|
18
20
|
}
|
19
21
|
|
20
22
|
type Unsub = () => void
|
@@ -34,13 +36,17 @@ export const connectDevtoolsToStore = ({
|
|
34
36
|
storeDevtoolsChannel,
|
35
37
|
store,
|
36
38
|
}: {
|
37
|
-
storeDevtoolsChannel: WebChannel.WebChannel<
|
39
|
+
storeDevtoolsChannel: WebChannel.WebChannel<
|
40
|
+
Devtools.ClientSession.MessageToApp,
|
41
|
+
Devtools.ClientSession.MessageFromApp
|
42
|
+
>
|
38
43
|
store: IStore
|
39
44
|
}) =>
|
40
45
|
Effect.gen(function* () {
|
41
46
|
const reactivityGraphSubcriptions: SubMap = new Map()
|
42
47
|
const liveQueriesSubscriptions: SubMap = new Map()
|
43
48
|
const debugInfoHistorySubscriptions: SubMap = new Map()
|
49
|
+
const syncHeadClientSessionSubscriptions: SubMap = new Map()
|
44
50
|
|
45
51
|
const { clientId, sessionId } = store.clientSession
|
46
52
|
|
@@ -49,13 +55,16 @@ export const connectDevtoolsToStore = ({
|
|
49
55
|
reactivityGraphSubcriptions.forEach((unsub) => unsub())
|
50
56
|
liveQueriesSubscriptions.forEach((unsub) => unsub())
|
51
57
|
debugInfoHistorySubscriptions.forEach((unsub) => unsub())
|
58
|
+
syncHeadClientSessionSubscriptions.forEach((unsub) => unsub())
|
52
59
|
}),
|
53
60
|
)
|
54
61
|
|
55
|
-
const
|
62
|
+
const handledRequestIds = new Set<RequestId>()
|
63
|
+
|
64
|
+
const sendToDevtools = (message: Devtools.ClientSession.MessageFromApp) =>
|
56
65
|
storeDevtoolsChannel.send(message).pipe(Effect.tapCauseLogPretty, Effect.runFork)
|
57
66
|
|
58
|
-
const onMessage = (decodedMessage: typeof Devtools.
|
67
|
+
const onMessage = (decodedMessage: typeof Devtools.ClientSession.MessageToApp.Type) => {
|
59
68
|
// console.debug('@livestore/livestore:store:devtools:onMessage', decodedMessage)
|
60
69
|
|
61
70
|
if (decodedMessage.clientId !== clientId || decodedMessage.sessionId !== sessionId) {
|
@@ -63,18 +72,29 @@ export const connectDevtoolsToStore = ({
|
|
63
72
|
return
|
64
73
|
}
|
65
74
|
|
66
|
-
if (decodedMessage._tag === 'LSD.Disconnect') {
|
75
|
+
if (decodedMessage._tag === 'LSD.ClientSession.Disconnect') {
|
67
76
|
// console.error('TODO handle disconnect properly in store')
|
68
77
|
return
|
69
78
|
}
|
70
79
|
|
71
80
|
const requestId = decodedMessage.requestId
|
72
81
|
|
82
|
+
// TODO we should try to move the duplicate message handling on the webmesh layer
|
83
|
+
// So far I could only observe this problem with webmesh proxy channels (e.g. for Expo)
|
84
|
+
// Proof: https://share.cleanshot.com/V9G87B0B
|
85
|
+
// Also see `leader-worker-devtools.ts` for same problem
|
86
|
+
if (handledRequestIds.has(requestId)) {
|
87
|
+
return
|
88
|
+
}
|
89
|
+
|
90
|
+
handledRequestIds.add(requestId)
|
91
|
+
|
73
92
|
const requestIdleCallback = globalThis.requestIdleCallback ?? ((cb: () => void) => cb())
|
74
93
|
|
75
94
|
switch (decodedMessage._tag) {
|
76
|
-
case 'LSD.ReactivityGraphSubscribe': {
|
95
|
+
case 'LSD.ClientSession.ReactivityGraphSubscribe': {
|
77
96
|
const includeResults = decodedMessage.includeResults
|
97
|
+
const { subscriptionId } = decodedMessage
|
78
98
|
|
79
99
|
const send = () =>
|
80
100
|
// In order to not add more work to the current tick, we use requestIdleCallback
|
@@ -82,12 +102,13 @@ export const connectDevtoolsToStore = ({
|
|
82
102
|
requestIdleCallback(
|
83
103
|
() =>
|
84
104
|
sendToDevtools(
|
85
|
-
Devtools.ReactivityGraphRes.make({
|
105
|
+
Devtools.ClientSession.ReactivityGraphRes.make({
|
86
106
|
reactivityGraph: store.reactivityGraph.getSnapshot({ includeResults }),
|
87
|
-
requestId,
|
107
|
+
requestId: nanoid(10),
|
88
108
|
clientId,
|
89
109
|
sessionId,
|
90
110
|
liveStoreVersion,
|
111
|
+
subscriptionId,
|
91
112
|
}),
|
92
113
|
),
|
93
114
|
{ timeout: 500 },
|
@@ -100,14 +121,14 @@ export const connectDevtoolsToStore = ({
|
|
100
121
|
// This might need to be tweaked further and possibly be exposed to the user in some way.
|
101
122
|
const throttledSend = throttle(send, 20)
|
102
123
|
|
103
|
-
reactivityGraphSubcriptions.set(
|
124
|
+
reactivityGraphSubcriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
|
104
125
|
|
105
126
|
break
|
106
127
|
}
|
107
|
-
case 'LSD.DebugInfoReq': {
|
128
|
+
case 'LSD.ClientSession.DebugInfoReq': {
|
108
129
|
sendToDevtools(
|
109
|
-
Devtools.DebugInfoRes.make({
|
110
|
-
debugInfo: store.
|
130
|
+
Devtools.ClientSession.DebugInfoRes.make({
|
131
|
+
debugInfo: store.sqliteDbWrapper.debugInfo,
|
111
132
|
requestId,
|
112
133
|
clientId,
|
113
134
|
sessionId,
|
@@ -116,28 +137,30 @@ export const connectDevtoolsToStore = ({
|
|
116
137
|
)
|
117
138
|
break
|
118
139
|
}
|
119
|
-
case 'LSD.DebugInfoHistorySubscribe': {
|
140
|
+
case 'LSD.ClientSession.DebugInfoHistorySubscribe': {
|
141
|
+
const { subscriptionId } = decodedMessage
|
120
142
|
const buffer: DebugInfo[] = []
|
121
143
|
let hasStopped = false
|
122
144
|
let tickHandle: number | undefined
|
123
145
|
|
124
146
|
const tick = () => {
|
125
|
-
buffer.push(store.
|
147
|
+
buffer.push(store.sqliteDbWrapper.debugInfo)
|
126
148
|
|
127
149
|
// NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
|
128
150
|
// will get the empty debug info
|
129
151
|
// TODO We need to come up with a more graceful way to do store. Probably via a single global
|
130
152
|
// `requestAnimationFrame` loop that is passed in somehow.
|
131
|
-
store.
|
153
|
+
store.sqliteDbWrapper.debugInfo = makeEmptyDebugInfo()
|
132
154
|
|
133
155
|
if (buffer.length > 10) {
|
134
156
|
sendToDevtools(
|
135
|
-
Devtools.DebugInfoHistoryRes.make({
|
157
|
+
Devtools.ClientSession.DebugInfoHistoryRes.make({
|
136
158
|
debugInfoHistory: buffer,
|
137
|
-
requestId,
|
159
|
+
requestId: nanoid(10),
|
138
160
|
clientId,
|
139
161
|
sessionId,
|
140
162
|
liveStoreVersion,
|
163
|
+
subscriptionId,
|
141
164
|
}),
|
142
165
|
)
|
143
166
|
buffer.length = 0
|
@@ -158,44 +181,53 @@ export const connectDevtoolsToStore = ({
|
|
158
181
|
}
|
159
182
|
}
|
160
183
|
|
161
|
-
debugInfoHistorySubscriptions.set(
|
184
|
+
debugInfoHistorySubscriptions.set(subscriptionId, unsub)
|
162
185
|
|
163
186
|
break
|
164
187
|
}
|
165
|
-
case 'LSD.DebugInfoHistoryUnsubscribe': {
|
166
|
-
|
167
|
-
//
|
168
|
-
|
169
|
-
debugInfoHistorySubscriptions.
|
188
|
+
case 'LSD.ClientSession.DebugInfoHistoryUnsubscribe': {
|
189
|
+
const { subscriptionId } = decodedMessage
|
190
|
+
// NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
|
191
|
+
// Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
|
192
|
+
debugInfoHistorySubscriptions.get(subscriptionId)?.()
|
193
|
+
debugInfoHistorySubscriptions.delete(subscriptionId)
|
170
194
|
break
|
171
195
|
}
|
172
|
-
case 'LSD.DebugInfoResetReq': {
|
173
|
-
store.
|
174
|
-
sendToDevtools(
|
196
|
+
case 'LSD.ClientSession.DebugInfoResetReq': {
|
197
|
+
store.sqliteDbWrapper.debugInfo.slowQueries.clear()
|
198
|
+
sendToDevtools(
|
199
|
+
Devtools.ClientSession.DebugInfoResetRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
|
200
|
+
)
|
175
201
|
break
|
176
202
|
}
|
177
|
-
case 'LSD.DebugInfoRerunQueryReq': {
|
203
|
+
case 'LSD.ClientSession.DebugInfoRerunQueryReq': {
|
178
204
|
const { queryStr, bindValues, queriedTables } = decodedMessage
|
179
|
-
store.
|
180
|
-
sendToDevtools(
|
205
|
+
store.sqliteDbWrapper.select(queryStr, bindValues, { queriedTables, skipCache: true })
|
206
|
+
sendToDevtools(
|
207
|
+
Devtools.ClientSession.DebugInfoRerunQueryRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
|
208
|
+
)
|
181
209
|
break
|
182
210
|
}
|
183
|
-
case 'LSD.ReactivityGraphUnsubscribe': {
|
184
|
-
|
185
|
-
//
|
186
|
-
|
211
|
+
case 'LSD.ClientSession.ReactivityGraphUnsubscribe': {
|
212
|
+
const { subscriptionId } = decodedMessage
|
213
|
+
// NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
|
214
|
+
// Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
|
215
|
+
reactivityGraphSubcriptions.get(subscriptionId)?.()
|
216
|
+
reactivityGraphSubcriptions.delete(subscriptionId)
|
187
217
|
break
|
188
218
|
}
|
189
|
-
case 'LSD.LiveQueriesSubscribe': {
|
219
|
+
case 'LSD.ClientSession.LiveQueriesSubscribe': {
|
220
|
+
const { subscriptionId } = decodedMessage
|
190
221
|
const send = () =>
|
191
222
|
requestIdleCallback(
|
192
223
|
() =>
|
193
224
|
sendToDevtools(
|
194
|
-
Devtools.LiveQueriesRes.make({
|
225
|
+
Devtools.ClientSession.LiveQueriesRes.make({
|
195
226
|
liveQueries: [...store.activeQueries].map((q) => ({
|
196
227
|
_tag: q._tag,
|
197
228
|
id: q.id,
|
198
229
|
label: q.label,
|
230
|
+
hash: q.def.hash,
|
199
231
|
runs: q.runs,
|
200
232
|
executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
|
201
233
|
lastestResult:
|
@@ -204,10 +236,11 @@ export const connectDevtoolsToStore = ({
|
|
204
236
|
: q.results$.previousResult,
|
205
237
|
activeSubscriptions: Array.from(q.activeSubscriptions),
|
206
238
|
})),
|
207
|
-
requestId,
|
239
|
+
requestId: nanoid(10),
|
208
240
|
liveStoreVersion,
|
209
241
|
clientId,
|
210
242
|
sessionId,
|
243
|
+
subscriptionId,
|
211
244
|
}),
|
212
245
|
),
|
213
246
|
{ timeout: 500 },
|
@@ -218,18 +251,63 @@ export const connectDevtoolsToStore = ({
|
|
218
251
|
// Same as in the reactivity graph subscription case above, we need to throttle the updates
|
219
252
|
const throttledSend = throttle(send, 20)
|
220
253
|
|
221
|
-
liveQueriesSubscriptions.set(
|
254
|
+
liveQueriesSubscriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
|
255
|
+
|
256
|
+
break
|
257
|
+
}
|
258
|
+
case 'LSD.ClientSession.LiveQueriesUnsubscribe': {
|
259
|
+
const { subscriptionId } = decodedMessage
|
260
|
+
// NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
|
261
|
+
// Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
|
262
|
+
liveQueriesSubscriptions.get(subscriptionId)?.()
|
263
|
+
liveQueriesSubscriptions.delete(subscriptionId)
|
264
|
+
break
|
265
|
+
}
|
266
|
+
case 'LSD.ClientSession.SyncHeadSubscribe': {
|
267
|
+
const { subscriptionId } = decodedMessage
|
268
|
+
const send = (syncState: SyncState.SyncState) =>
|
269
|
+
sendToDevtools(
|
270
|
+
Devtools.ClientSession.SyncHeadRes.make({
|
271
|
+
local: syncState.localHead,
|
272
|
+
upstream: syncState.upstreamHead,
|
273
|
+
requestId: nanoid(10),
|
274
|
+
clientId,
|
275
|
+
sessionId,
|
276
|
+
liveStoreVersion,
|
277
|
+
subscriptionId,
|
278
|
+
}),
|
279
|
+
)
|
280
|
+
|
281
|
+
send(store.syncProcessor.syncState.pipe(Effect.runSync))
|
282
|
+
|
283
|
+
syncHeadClientSessionSubscriptions.set(
|
284
|
+
subscriptionId,
|
285
|
+
store.syncProcessor.syncState.changes.pipe(
|
286
|
+
Stream.tap((syncState) => send(syncState)),
|
287
|
+
Stream.runDrain,
|
288
|
+
Effect.interruptible,
|
289
|
+
Effect.tapCauseLogPretty,
|
290
|
+
Effect.runCallback,
|
291
|
+
),
|
292
|
+
)
|
222
293
|
|
223
294
|
break
|
224
295
|
}
|
225
|
-
case 'LSD.
|
226
|
-
|
227
|
-
//
|
228
|
-
|
229
|
-
|
296
|
+
case 'LSD.ClientSession.SyncHeadUnsubscribe': {
|
297
|
+
const { subscriptionId } = decodedMessage
|
298
|
+
// NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
|
299
|
+
// Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
|
300
|
+
syncHeadClientSessionSubscriptions.get(subscriptionId)?.()
|
301
|
+
syncHeadClientSessionSubscriptions.delete(subscriptionId)
|
230
302
|
break
|
231
303
|
}
|
232
|
-
|
304
|
+
case 'LSD.ClientSession.Ping': {
|
305
|
+
sendToDevtools(Devtools.ClientSession.Pong.make({ requestId, clientId, sessionId, liveStoreVersion }))
|
306
|
+
break
|
307
|
+
}
|
308
|
+
default: {
|
309
|
+
console.warn(`[LSD.ClientSession] Unknown message`, decodedMessage)
|
310
|
+
}
|
233
311
|
}
|
234
312
|
}
|
235
313
|
|