@livestore/livestore 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/README.md +0 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/QueryCache.js +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/SqliteDbWrapper.d.ts +5 -5
- package/dist/SqliteDbWrapper.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.js +15 -13
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +24 -6
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +134 -6
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +190 -11
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/LiveStore.test.d.ts +2 -0
- package/dist/effect/LiveStore.test.d.ts.map +1 -0
- package/dist/effect/LiveStore.test.js +41 -0
- package/dist/effect/LiveStore.test.js.map +1 -0
- package/dist/effect/mod.d.ts +1 -1
- package/dist/effect/mod.d.ts.map +1 -1
- package/dist/effect/mod.js +3 -1
- package/dist/effect/mod.js.map +1 -1
- package/dist/live-queries/base-class.d.ts +129 -9
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +30 -4
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/client-document-get-query.d.ts +1 -1
- package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
- package/dist/live-queries/client-document-get-query.js +4 -3
- package/dist/live-queries/client-document-get-query.js.map +1 -1
- package/dist/live-queries/computed.d.ts +56 -0
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +62 -6
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.d.ts +2 -2
- package/dist/live-queries/db-query.d.ts.map +1 -1
- package/dist/live-queries/db-query.js +41 -30
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +112 -29
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/live-queries/signal.d.ts +49 -0
- package/dist/live-queries/signal.d.ts.map +1 -1
- package/dist/live-queries/signal.js +51 -2
- package/dist/live-queries/signal.js.map +1 -1
- package/dist/live-queries/signal.test.js +4 -4
- package/dist/live-queries/signal.test.js.map +1 -1
- package/dist/mod.d.ts +3 -3
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +3 -2
- package/dist/mod.js.map +1 -1
- package/dist/reactive.d.ts +10 -10
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +29 -27
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +2 -2
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +215 -0
- package/dist/store/StoreRegistry.d.ts.map +1 -0
- package/dist/store/StoreRegistry.js +267 -0
- package/dist/store/StoreRegistry.js.map +1 -0
- package/dist/store/StoreRegistry.test.d.ts +2 -0
- package/dist/store/StoreRegistry.test.d.ts.map +1 -0
- package/dist/store/StoreRegistry.test.js +384 -0
- package/dist/store/StoreRegistry.test.js.map +1 -0
- package/dist/store/create-store.d.ts +99 -22
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +85 -37
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +6 -18
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +61 -19
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-eventstream.test.d.ts +2 -0
- package/dist/store/store-eventstream.test.d.ts.map +1 -0
- package/dist/store/store-eventstream.test.js +65 -0
- package/dist/store/store-eventstream.test.js.map +1 -0
- package/dist/store/store-types.d.ts +271 -31
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js +41 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store-types.test.d.ts +2 -0
- package/dist/store/store-types.test.d.ts.map +1 -0
- package/dist/store/store-types.test.js +39 -0
- package/dist/store/store-types.test.js.map +1 -0
- package/dist/store/store.d.ts +255 -68
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +533 -186
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.d.ts.map +1 -1
- package/dist/utils/dev.js +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/stack-info.d.ts.map +1 -1
- package/dist/utils/stack-info.js +3 -2
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +46 -39
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js +7 -0
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +5 -5
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +50 -18
- package/src/QueryCache.ts +1 -1
- package/src/SqliteDbWrapper.test.ts +30 -6
- package/src/SqliteDbWrapper.ts +21 -17
- package/src/ambient.d.ts +0 -7
- package/src/effect/LiveStore.test.ts +61 -0
- package/src/effect/LiveStore.ts +426 -22
- package/src/effect/mod.ts +13 -1
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +814 -178
- package/src/live-queries/base-class.ts +162 -32
- package/src/live-queries/client-document-get-query.ts +6 -4
- package/src/live-queries/computed.ts +65 -8
- package/src/live-queries/db-query.test.ts +168 -30
- package/src/live-queries/db-query.ts +58 -43
- package/src/live-queries/signal.test.ts +5 -4
- package/src/live-queries/signal.ts +52 -3
- package/src/mod.ts +19 -2
- package/src/reactive.test.ts +3 -2
- package/src/reactive.ts +58 -49
- package/src/store/StoreRegistry.test.ts +543 -0
- package/src/store/StoreRegistry.ts +429 -0
- package/src/store/create-store.ts +227 -74
- package/src/store/devtools.ts +305 -261
- package/src/store/store-eventstream.test.ts +123 -0
- package/src/store/store-types.test.ts +52 -0
- package/src/store/store-types.ts +318 -44
- package/src/store/store.ts +695 -283
- package/src/utils/dev.ts +3 -4
- package/src/utils/stack-info.ts +5 -2
- package/src/utils/tests/fixture.ts +9 -1
- package/src/utils/tests/otel.ts +8 -7
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import * as otel from '@opentelemetry/api'
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
type Adapter,
|
|
5
|
+
type BackendIdMismatchError,
|
|
3
6
|
type BootStatus,
|
|
4
7
|
type ClientSession,
|
|
5
8
|
type ClientSessionDevtoolsChannel,
|
|
6
|
-
type ClientSessionSyncProcessorSimulationParams,
|
|
7
9
|
type IntentionalShutdownCause,
|
|
8
|
-
type InvalidPullError,
|
|
9
|
-
type IsOfflineError,
|
|
10
10
|
type MaterializeError,
|
|
11
11
|
type MigrationsReport,
|
|
12
12
|
provideOtel,
|
|
13
|
-
type
|
|
14
|
-
|
|
13
|
+
type ServerAheadError,
|
|
14
|
+
UnknownError,
|
|
15
|
+
type LogConfig,
|
|
15
16
|
} from '@livestore/common'
|
|
16
17
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
17
18
|
import { isDevEnv, LS_DEV, omitUndefineds } from '@livestore/utils'
|
|
@@ -23,47 +24,76 @@ import {
|
|
|
23
24
|
Fiber,
|
|
24
25
|
identity,
|
|
25
26
|
Layer,
|
|
26
|
-
Logger,
|
|
27
|
-
LogLevel,
|
|
28
27
|
OtelTracer,
|
|
29
28
|
Queue,
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
References,
|
|
30
|
+
Schema,
|
|
32
31
|
Scope,
|
|
33
32
|
TaskTracing,
|
|
34
33
|
} from '@livestore/utils/effect'
|
|
35
34
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
36
|
-
import * as otel from '@opentelemetry/api'
|
|
37
35
|
|
|
38
36
|
import { connectDevtoolsToStore } from './devtools.ts'
|
|
39
|
-
import { Store } from './store.ts'
|
|
40
37
|
import type {
|
|
41
38
|
LiveStoreContextRunning as LiveStoreContextRunning_,
|
|
42
39
|
OtelOptions,
|
|
43
40
|
ShutdownDeferred,
|
|
44
41
|
} from './store-types.ts'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
import { StoreInternalsSymbol } from './store-types.ts'
|
|
43
|
+
import { STORE_DEFAULT_PARAMS, Store } from './store.ts'
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Hard upper bound (ms) for the detached shutdown drain. A dead/unresponsive leader must not keep
|
|
47
|
+
* the drain (and therefore the lifetime scope) alive forever; after this bound the scope is
|
|
48
|
+
* force-closed. Kept comfortably above the 1s caller-side soft wait so that a still-progressing
|
|
49
|
+
* in-flight leader push is allowed to finish rather than being interrupted.
|
|
50
|
+
*/
|
|
51
|
+
const SHUTDOWN_DRAIN_HARD_TIMEOUT_MS = 30_000
|
|
52
|
+
|
|
53
|
+
declare global {
|
|
54
|
+
/** Store instances for console debugging */
|
|
55
|
+
var __debugLiveStore: Record<string, Store<any, any>> | undefined
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
/**
|
|
59
|
+
* @deprecated Use `makeStoreContext()` from `@livestore/livestore/effect` instead.
|
|
60
|
+
* This service doesn't preserve schema types. See the Effect integration docs for migration.
|
|
61
|
+
*
|
|
62
|
+
* @example Migration
|
|
63
|
+
* ```ts
|
|
64
|
+
* // Before (untyped)
|
|
65
|
+
* import { LiveStoreContextRunning } from '@livestore/livestore/effect'
|
|
66
|
+
* const { store } = yield* LiveStoreContextRunning
|
|
67
|
+
*
|
|
68
|
+
* // After (typed)
|
|
69
|
+
* import { makeStoreContext } from '@livestore/livestore/effect'
|
|
70
|
+
* const AppStore = makeStoreContext<typeof schema>()('app')
|
|
71
|
+
* const { store } = yield* AppStore.Tag
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export class LiveStoreContextRunning extends Context.Service<LiveStoreContextRunning, LiveStoreContextRunning_>()(
|
|
75
|
+
'@livestore/livestore/effect/LiveStoreContextRunning',
|
|
76
|
+
) {
|
|
54
77
|
static fromDeferred = Effect.gen(function* () {
|
|
55
78
|
const deferred = yield* DeferredStoreContext
|
|
56
|
-
const ctx = yield* deferred
|
|
57
|
-
return Layer.succeed(LiveStoreContextRunning, ctx)
|
|
58
|
-
}).pipe(Layer.
|
|
79
|
+
const ctx = yield* Deferred.await(deferred)
|
|
80
|
+
return Layer.succeed(LiveStoreContextRunning, LiveStoreContextRunning.of(ctx))
|
|
81
|
+
}).pipe(Layer.unwrap)
|
|
59
82
|
}
|
|
60
83
|
|
|
61
|
-
|
|
84
|
+
/**
|
|
85
|
+
* @deprecated Use `StoreContext.DeferredTag` from `makeStoreContext()` instead.
|
|
86
|
+
*/
|
|
87
|
+
export class DeferredStoreContext extends Context.Service<
|
|
62
88
|
DeferredStoreContext,
|
|
63
|
-
Deferred.Deferred<LiveStoreContextRunning['
|
|
64
|
-
>() {}
|
|
65
|
-
|
|
66
|
-
export type LiveStoreContextProps<
|
|
89
|
+
Deferred.Deferred<LiveStoreContextRunning['Service'], UnknownError>
|
|
90
|
+
>()('@livestore/livestore/effect/DeferredStoreContext') {}
|
|
91
|
+
|
|
92
|
+
export type LiveStoreContextProps<
|
|
93
|
+
TSchema extends LiveStoreSchema,
|
|
94
|
+
TContext = {},
|
|
95
|
+
TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
|
|
96
|
+
> = {
|
|
67
97
|
schema: TSchema
|
|
68
98
|
/**
|
|
69
99
|
* The `storeId` can be used to isolate multiple stores from each other.
|
|
@@ -88,12 +118,48 @@ export type LiveStoreContextProps<TSchema extends LiveStoreSchema, TContext = {}
|
|
|
88
118
|
disableDevtools?: boolean | 'auto'
|
|
89
119
|
onBootStatus?: (status: BootStatus) => void
|
|
90
120
|
batchUpdates: (run: () => void) => void
|
|
121
|
+
/**
|
|
122
|
+
* Schema describing the shape of the sync payload and used to encode it.
|
|
123
|
+
*
|
|
124
|
+
* - If omitted, `Schema.Json` is used (no additional typing/validation).
|
|
125
|
+
* - Prefer exporting a schema from your app (e.g. `export const SyncPayload = Schema.Struct({ authToken: Schema.String })`)
|
|
126
|
+
* and pass it here to get end-to-end type safety and validation.
|
|
127
|
+
*/
|
|
128
|
+
syncPayloadSchema?: TSyncPayloadSchema
|
|
129
|
+
/**
|
|
130
|
+
* Payload that is sent to the sync backend when connecting
|
|
131
|
+
*
|
|
132
|
+
* - Its TypeScript type is inferred from `syncPayloadSchema` (i.e. `typeof SyncPayload.Type`).
|
|
133
|
+
* - At runtime this value is encoded with `syncPayloadSchema` before being handed to the adapter.
|
|
134
|
+
*
|
|
135
|
+
* Example:
|
|
136
|
+
* const SyncPayload = Schema.Struct({ authToken: Schema.String })
|
|
137
|
+
* useStore({ ..., syncPayloadSchema: SyncPayload, syncPayload: { authToken: '...' } })
|
|
138
|
+
*/
|
|
139
|
+
syncPayload?: TSyncPayloadSchema['Type']
|
|
91
140
|
}
|
|
92
141
|
|
|
93
|
-
export interface CreateStoreOptions<
|
|
142
|
+
export interface CreateStoreOptions<
|
|
143
|
+
TSchema extends LiveStoreSchema,
|
|
144
|
+
TContext = {},
|
|
145
|
+
TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
|
|
146
|
+
>
|
|
147
|
+
extends LogConfig.LoggerOptions {
|
|
148
|
+
/** The LiveStore schema defining tables, events, and materializers. */
|
|
94
149
|
schema: TSchema
|
|
150
|
+
/** Adapter used for data storage and synchronization. */
|
|
95
151
|
adapter: Adapter
|
|
152
|
+
/**
|
|
153
|
+
* Unique identifier for the Store instance, stable for its lifetime.
|
|
154
|
+
*
|
|
155
|
+
* - **Valid characters**: Only alphanumeric characters, underscores (`_`), and hyphens (`-`)
|
|
156
|
+
* are allowed. Must match `/^[a-zA-Z0-9_-]+$/`.
|
|
157
|
+
* - **Globally unique**: Use globally unique IDs (e.g., nanoid) to prevent collisions across stores.
|
|
158
|
+
* - **Use namespaces**: Prefix to avoid collisions and for easier identification when debugging
|
|
159
|
+
* (e.g., `app-root`, `workspace-abc123`, `issue-456`)
|
|
160
|
+
*/
|
|
96
161
|
storeId: string
|
|
162
|
+
/** User-defined context that will be attached to the created Store (e.g. for dependency injection). */
|
|
97
163
|
context?: TContext
|
|
98
164
|
boot?: (
|
|
99
165
|
store: Store<TSchema, TContext>,
|
|
@@ -102,6 +168,19 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
|
|
|
102
168
|
parentSpan: otel.Span
|
|
103
169
|
},
|
|
104
170
|
) => Effect.SyncOrPromiseOrEffect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
|
|
171
|
+
onBootStatus?: (status: BootStatus) => void
|
|
172
|
+
/**
|
|
173
|
+
* Needed in React so LiveStore can apply multiple events in a single render.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* // With React DOM
|
|
178
|
+
* import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
|
|
179
|
+
*
|
|
180
|
+
* // With React Native
|
|
181
|
+
* import { unstable_batchedUpdates as batchUpdates } from 'react-native'
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
105
184
|
batchUpdates?: (run: () => void) => void
|
|
106
185
|
/**
|
|
107
186
|
* Whether to disable devtools.
|
|
@@ -109,7 +188,6 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
|
|
|
109
188
|
* @default 'auto'
|
|
110
189
|
*/
|
|
111
190
|
disableDevtools?: boolean | 'auto'
|
|
112
|
-
onBootStatus?: (status: BootStatus) => void
|
|
113
191
|
shutdownDeferred?: ShutdownDeferred
|
|
114
192
|
/**
|
|
115
193
|
* Currently only used in the web adapter:
|
|
@@ -119,42 +197,65 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
|
|
|
119
197
|
*/
|
|
120
198
|
confirmUnsavedChanges?: boolean
|
|
121
199
|
/**
|
|
122
|
-
*
|
|
200
|
+
* Schema describing the shape of the sync payload and used to encode it.
|
|
201
|
+
*
|
|
202
|
+
* - If omitted, `Schema.Json` is used (no additional typing/validation).
|
|
203
|
+
* - Prefer exporting a schema from your app (e.g. `export const SyncPayload = Schema.Struct({ authToken: Schema.String })`)
|
|
204
|
+
* and pass it here to get end-to-end type safety and validation.
|
|
205
|
+
*/
|
|
206
|
+
syncPayloadSchema?: TSyncPayloadSchema
|
|
207
|
+
/**
|
|
208
|
+
* Payload that is sent to the sync backend when connecting
|
|
209
|
+
*
|
|
210
|
+
* - Its TypeScript type is inferred from `syncPayloadSchema` (i.e. `typeof SyncPayload.Type`).
|
|
211
|
+
* - At runtime this value is encoded with `syncPayloadSchema` and carried through the adapter
|
|
212
|
+
* to the backend where it can be decoded with the same schema.
|
|
123
213
|
*
|
|
124
214
|
* @default undefined
|
|
125
215
|
*/
|
|
126
|
-
syncPayload?:
|
|
216
|
+
syncPayload?: TSyncPayloadSchema['Type']
|
|
217
|
+
/** Options provided to the Store constructor. */
|
|
127
218
|
params?: {
|
|
219
|
+
/** Max events pushed to the leader per write batch. */
|
|
128
220
|
leaderPushBatchSize?: number
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
221
|
+
/** Chunk size used when the stream replays confirmed events. */
|
|
222
|
+
eventQueryBatchSize?: number
|
|
132
223
|
}
|
|
133
224
|
debug?: {
|
|
134
225
|
instanceId?: string
|
|
135
226
|
}
|
|
136
227
|
}
|
|
137
228
|
|
|
229
|
+
export type CreateStoreOptionsPromise<
|
|
230
|
+
TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
|
|
231
|
+
TContext = {},
|
|
232
|
+
TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
|
|
233
|
+
> = CreateStoreOptions<TSchema, TContext, TSyncPayloadSchema> & {
|
|
234
|
+
signal?: AbortSignal
|
|
235
|
+
otelOptions?: Partial<OtelOptions>
|
|
236
|
+
}
|
|
237
|
+
|
|
138
238
|
/** Create a new LiveStore Store */
|
|
139
|
-
export const createStorePromise = async <
|
|
239
|
+
export const createStorePromise = async <
|
|
240
|
+
TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
|
|
241
|
+
TContext = {},
|
|
242
|
+
TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
|
|
243
|
+
>({
|
|
140
244
|
signal,
|
|
141
245
|
otelOptions,
|
|
142
246
|
...options
|
|
143
|
-
}:
|
|
144
|
-
signal?: AbortSignal
|
|
145
|
-
otelOptions?: Partial<OtelOptions>
|
|
146
|
-
}): Promise<Store<TSchema, TContext>> =>
|
|
247
|
+
}: CreateStoreOptionsPromise<TSchema, TContext, TSyncPayloadSchema>): Promise<Store<TSchema, TContext>> =>
|
|
147
248
|
Effect.gen(function* () {
|
|
148
249
|
const scope = yield* Scope.make()
|
|
149
|
-
const
|
|
250
|
+
const services = yield* Effect.context()
|
|
150
251
|
|
|
151
252
|
if (signal !== undefined) {
|
|
152
253
|
signal.addEventListener('abort', () => {
|
|
153
|
-
Scope.close(scope, Exit.void).pipe(Effect.tapCauseLogPretty,
|
|
254
|
+
Scope.close(scope, Exit.void).pipe(Effect.tapCauseLogPretty, Effect.runForkWith(services))
|
|
154
255
|
})
|
|
155
256
|
}
|
|
156
257
|
|
|
157
|
-
return yield* createStore({ ...options }).pipe(Scope.
|
|
258
|
+
return yield* createStore({ ...options }).pipe(Scope.provide(scope))
|
|
158
259
|
}).pipe(
|
|
159
260
|
Effect.withSpan('createStore', {
|
|
160
261
|
attributes: { storeId: options.storeId, disableDevtools: options.disableDevtools },
|
|
@@ -162,12 +263,20 @@ export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveS
|
|
|
162
263
|
provideOtel(omitUndefineds({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer })),
|
|
163
264
|
Effect.tapCauseLogPretty,
|
|
164
265
|
Effect.annotateLogs({ thread: 'window' }),
|
|
165
|
-
Effect.provide(
|
|
166
|
-
|
|
266
|
+
Effect.provide(
|
|
267
|
+
Layer.mergeAll(
|
|
268
|
+
options.logger ?? Layer.empty,
|
|
269
|
+
Layer.succeed(References.MinimumLogLevel, options.logLevel ?? (isDevEnv() === true ? 'Debug' : 'Info')),
|
|
270
|
+
),
|
|
271
|
+
),
|
|
167
272
|
Effect.runPromise,
|
|
168
273
|
)
|
|
169
274
|
|
|
170
|
-
export const createStore = <
|
|
275
|
+
export const createStore = <
|
|
276
|
+
TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
|
|
277
|
+
TContext = {},
|
|
278
|
+
TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
|
|
279
|
+
>({
|
|
171
280
|
schema,
|
|
172
281
|
adapter,
|
|
173
282
|
storeId,
|
|
@@ -181,9 +290,10 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
|
|
|
181
290
|
debug,
|
|
182
291
|
confirmUnsavedChanges = true,
|
|
183
292
|
syncPayload,
|
|
184
|
-
|
|
293
|
+
syncPayloadSchema,
|
|
294
|
+
}: CreateStoreOptions<TSchema, TContext, TSyncPayloadSchema>): Effect.Effect<
|
|
185
295
|
Store<TSchema, TContext>,
|
|
186
|
-
|
|
296
|
+
UnknownError,
|
|
187
297
|
Scope.Scope | OtelTracer.OtelTracer
|
|
188
298
|
> =>
|
|
189
299
|
Effect.gen(function* () {
|
|
@@ -194,17 +304,20 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
|
|
|
194
304
|
yield* Effect.addFinalizer((_) => Scope.close(lifetimeScope, _))
|
|
195
305
|
|
|
196
306
|
const debugInstanceId = debug?.instanceId ?? nanoid(10)
|
|
307
|
+
const resolvedSyncPayloadSchema = (syncPayloadSchema ?? Schema.Json) as TSyncPayloadSchema
|
|
197
308
|
|
|
198
309
|
return yield* Effect.gen(function* () {
|
|
199
310
|
const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
|
|
200
311
|
const otelRootSpanContext = otel.trace.setSpan(otel.context.active(), span)
|
|
201
312
|
const otelTracer = yield* OtelTracer.OtelTracer
|
|
202
313
|
|
|
203
|
-
const bootStatusQueue = yield* Queue.unbounded<BootStatus>()
|
|
314
|
+
const bootStatusQueue = yield* Effect.acquireRelease(Queue.unbounded<BootStatus>(), Queue.shutdown)
|
|
204
315
|
|
|
205
316
|
yield* Queue.take(bootStatusQueue).pipe(
|
|
206
317
|
Effect.tapSync((status) => onBootStatus?.(status)),
|
|
207
|
-
Effect.tap((status) =>
|
|
318
|
+
Effect.tap((status) =>
|
|
319
|
+
status.stage === 'done' ? Queue.shutdown(bootStatusQueue).pipe(Effect.asVoid) : Effect.void,
|
|
320
|
+
),
|
|
208
321
|
Effect.forever,
|
|
209
322
|
Effect.tapCauseLogPretty,
|
|
210
323
|
Effect.forkScoped,
|
|
@@ -214,35 +327,52 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
|
|
|
214
327
|
|
|
215
328
|
const connectDevtoolsToStore_ = (storeDevtoolsChannel: ClientSessionDevtoolsChannel) =>
|
|
216
329
|
Effect.gen(function* () {
|
|
217
|
-
const store = yield* storeDeferred
|
|
330
|
+
const store = yield* Deferred.await(storeDeferred)
|
|
218
331
|
yield* connectDevtoolsToStore({ storeDevtoolsChannel, store })
|
|
219
332
|
})
|
|
220
333
|
|
|
221
|
-
const
|
|
334
|
+
const services = yield* Effect.context<Scope.Scope>()
|
|
335
|
+
let shutdownSyncProcessor: ((exit: Exit.Exit<unknown, unknown>) => Effect.Effect<void>) | undefined
|
|
222
336
|
|
|
223
337
|
const shutdown = (
|
|
224
|
-
exit: Exit.Exit<
|
|
225
|
-
IntentionalShutdownCause,
|
|
226
|
-
UnexpectedError | MaterializeError | SyncError | InvalidPullError | IsOfflineError
|
|
227
|
-
>,
|
|
338
|
+
exit: Exit.Exit<IntentionalShutdownCause, UnknownError | MaterializeError | BackendIdMismatchError>,
|
|
228
339
|
) =>
|
|
229
340
|
Effect.gen(function* () {
|
|
230
|
-
|
|
341
|
+
// Hard outer bound on the DETACHED teardown: the processor drain `awaitEmpty`s the
|
|
342
|
+
// leader-push worker, which never completes if the leader is dead/unresponsive. Without a
|
|
343
|
+
// bound the drain would block forever, so `Scope.close(lifetimeScope)` (in `ensuring`)
|
|
344
|
+
// would never run and the lifetime scope + its resources would leak indefinitely (a later
|
|
345
|
+
// `createStore`/registry dispose on the same `storeId` would observe a never-closed store).
|
|
346
|
+
// Drain up to the bound, then force-close the scope regardless. The bound exceeds the
|
|
347
|
+
// caller-side wait below so an in-flight (but progressing) push is not cut short.
|
|
348
|
+
const closeFiber = yield* (shutdownSyncProcessor?.(exit) ?? Effect.void).pipe(
|
|
349
|
+
Effect.timeout(SHUTDOWN_DRAIN_HARD_TIMEOUT_MS),
|
|
350
|
+
Effect.catchTag('TimeoutError', () =>
|
|
351
|
+
Effect.logError(
|
|
352
|
+
`@livestore/livestore:shutdown: drain exceeded hard bound of ${SHUTDOWN_DRAIN_HARD_TIMEOUT_MS}ms; forcing scope close`,
|
|
353
|
+
),
|
|
354
|
+
),
|
|
355
|
+
Effect.ensuring(Scope.close(lifetimeScope, exit)),
|
|
356
|
+
Effect.forkDetach,
|
|
357
|
+
)
|
|
358
|
+
// Caller-side soft wait: stop blocking the shutdown() caller after 1s without cancelling
|
|
359
|
+
// the detached teardown above (which remains bounded by SHUTDOWN_DRAIN_HARD_TIMEOUT_MS).
|
|
360
|
+
yield* Fiber.join(closeFiber).pipe(
|
|
231
361
|
Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
|
|
232
362
|
Effect.timeout(1000),
|
|
233
|
-
Effect.catchTag('
|
|
363
|
+
Effect.catchTag('TimeoutError', () =>
|
|
234
364
|
Effect.logError('@livestore/livestore:shutdown: Timed out after 1 second'),
|
|
235
365
|
),
|
|
236
366
|
)
|
|
237
367
|
|
|
238
|
-
if (shutdownDeferred) {
|
|
368
|
+
if (shutdownDeferred !== undefined) {
|
|
239
369
|
yield* Deferred.done(shutdownDeferred, exit)
|
|
240
370
|
}
|
|
241
371
|
|
|
242
372
|
yield* Effect.logDebug('LiveStore shutdown complete')
|
|
243
373
|
}).pipe(
|
|
244
374
|
Effect.withSpan('@livestore/livestore:shutdown'),
|
|
245
|
-
Effect.provide(
|
|
375
|
+
Effect.provide(services),
|
|
246
376
|
Effect.tapCauseLogPretty,
|
|
247
377
|
// Given that the shutdown flow might also interrupt the effect that is calling the shutdown,
|
|
248
378
|
// we want to detach the shutdown effect so it's not interrupted by itself
|
|
@@ -250,6 +380,11 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
|
|
|
250
380
|
Fiber.join,
|
|
251
381
|
)
|
|
252
382
|
|
|
383
|
+
const syncPayloadEncoded =
|
|
384
|
+
syncPayload === undefined
|
|
385
|
+
? undefined
|
|
386
|
+
: yield* Schema.encodeEffect(resolvedSyncPayloadSchema)(syncPayload).pipe(UnknownError.mapToUnknownError)
|
|
387
|
+
|
|
253
388
|
const clientSession: ClientSession = yield* adapter({
|
|
254
389
|
schema,
|
|
255
390
|
storeId,
|
|
@@ -258,10 +393,11 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
|
|
|
258
393
|
shutdown,
|
|
259
394
|
connectDevtoolsToStore: connectDevtoolsToStore_,
|
|
260
395
|
debugInstanceId,
|
|
261
|
-
|
|
396
|
+
syncPayloadSchema: resolvedSyncPayloadSchema,
|
|
397
|
+
syncPayloadEncoded,
|
|
262
398
|
}).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
|
|
263
399
|
|
|
264
|
-
if (LS_DEV && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
|
|
400
|
+
if (LS_DEV === true && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
|
|
265
401
|
yield* Effect.logDebug(
|
|
266
402
|
'[@livestore/livestore:createStore] migrationsReport',
|
|
267
403
|
...clientSession.leaderThread.initialState.migrationsReport.migrations.map((m) =>
|
|
@@ -277,51 +413,68 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
|
|
|
277
413
|
schema,
|
|
278
414
|
context,
|
|
279
415
|
otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
|
|
280
|
-
effectContext: { lifetimeScope,
|
|
416
|
+
effectContext: { lifetimeScope, services },
|
|
281
417
|
// TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
|
|
282
418
|
// But for now this is a good enough approximation with little downsides
|
|
283
|
-
__runningInDevtools: getDevtoolsEnabled(disableDevtools)
|
|
419
|
+
__runningInDevtools: !getDevtoolsEnabled(disableDevtools),
|
|
284
420
|
confirmUnsavedChanges,
|
|
285
421
|
// NOTE during boot we're not yet executing events in a batched context
|
|
286
422
|
// but only set the provided `batchUpdates` function after boot
|
|
287
423
|
batchUpdates: (run) => run(),
|
|
288
424
|
storeId,
|
|
289
425
|
params: {
|
|
290
|
-
leaderPushBatchSize: params?.leaderPushBatchSize ??
|
|
291
|
-
|
|
426
|
+
leaderPushBatchSize: params?.leaderPushBatchSize ?? STORE_DEFAULT_PARAMS.leaderPushBatchSize,
|
|
427
|
+
eventQueryBatchSize: params?.eventQueryBatchSize ?? STORE_DEFAULT_PARAMS.eventQueryBatchSize,
|
|
292
428
|
},
|
|
293
429
|
})
|
|
430
|
+
shutdownSyncProcessor = store[StoreInternalsSymbol].syncProcessor.shutdown
|
|
294
431
|
|
|
295
432
|
// Starts background fibers (syncing, event processing, etc) for store
|
|
296
|
-
yield* store.boot
|
|
433
|
+
yield* store[StoreInternalsSymbol].boot
|
|
297
434
|
|
|
298
435
|
if (boot !== undefined) {
|
|
299
436
|
// TODO also incorporate `boot` function progress into `bootStatusQueue`
|
|
300
|
-
|
|
437
|
+
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- user boot errors are immediately normalized to LiveStore UnknownError
|
|
438
|
+
yield* Effect.trySyncOrPromiseOrEffect(() =>
|
|
301
439
|
boot(store, { migrationsReport: clientSession.leaderThread.initialState.migrationsReport, parentSpan: span }),
|
|
302
440
|
).pipe(
|
|
303
|
-
|
|
304
|
-
Effect.provide(
|
|
441
|
+
UnknownError.mapToUnknownError,
|
|
442
|
+
Effect.provide(
|
|
443
|
+
Layer.succeed(
|
|
444
|
+
LiveStoreContextRunning,
|
|
445
|
+
LiveStoreContextRunning.of({ stage: 'running', store: store as any as Store }),
|
|
446
|
+
),
|
|
447
|
+
),
|
|
305
448
|
Effect.withSpan('createStore:boot'),
|
|
306
449
|
)
|
|
307
450
|
}
|
|
308
451
|
|
|
309
452
|
// NOTE it's important to yield here to allow the forked Effect in the store constructor to run
|
|
310
|
-
yield* Effect.yieldNow
|
|
453
|
+
yield* Effect.yieldNow
|
|
311
454
|
|
|
312
455
|
if (batchUpdates !== undefined) {
|
|
313
456
|
// Replacing the default batchUpdates function with the provided one after boot
|
|
314
|
-
store.reactivityGraph.context!.effectsWrapper = batchUpdates
|
|
457
|
+
store[StoreInternalsSymbol].reactivityGraph.context!.effectsWrapper = batchUpdates
|
|
315
458
|
}
|
|
316
459
|
|
|
317
460
|
yield* Deferred.succeed(storeDeferred, store as any as Store)
|
|
318
461
|
|
|
462
|
+
// Expose store on globalThis for console debugging
|
|
463
|
+
globalThis.__debugLiveStore ??= {}
|
|
464
|
+
globalThis.__debugLiveStore[storeId] = store
|
|
465
|
+
|
|
466
|
+
yield* Effect.addFinalizer(() =>
|
|
467
|
+
Effect.sync(() => {
|
|
468
|
+
delete globalThis.__debugLiveStore?.[storeId]
|
|
469
|
+
}),
|
|
470
|
+
)
|
|
471
|
+
|
|
319
472
|
return store
|
|
320
473
|
}).pipe(
|
|
321
474
|
Effect.withSpan('createStore', { attributes: { debugInstanceId, storeId } }),
|
|
322
475
|
Effect.annotateLogs({ debugInstanceId, storeId }),
|
|
323
|
-
LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
|
|
324
|
-
Scope.
|
|
476
|
+
LS_DEV === true ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
|
|
477
|
+
Scope.provide(lifetimeScope),
|
|
325
478
|
)
|
|
326
479
|
})
|
|
327
480
|
|
|
@@ -329,8 +482,8 @@ const validateStoreId = (storeId: string) =>
|
|
|
329
482
|
Effect.gen(function* () {
|
|
330
483
|
const validChars = /^[a-zA-Z0-9_-]+$/
|
|
331
484
|
|
|
332
|
-
if (
|
|
333
|
-
return yield*
|
|
485
|
+
if (validChars.test(storeId) === false) {
|
|
486
|
+
return yield* UnknownError.make({
|
|
334
487
|
cause: `Invalid storeId: ${storeId}. Only alphanumeric characters, underscores, and hyphens are allowed.`,
|
|
335
488
|
payload: { storeId },
|
|
336
489
|
})
|