import type { UnknownError } from '@livestore/common' import type { LiveStoreEvent, LiveStoreSchema } from '@livestore/common/schema' import { omitUndefineds } from '@livestore/utils' import type { Cause, OtelTracer, Scope } from '@livestore/utils/effect' import { Context, Deferred, Duration, Effect, Layer, pipe } from '@livestore/utils/effect' import type { LiveStoreContextProps } from '../store/create-store.ts' import { createStore, DeferredStoreContext, LiveStoreContextRunning } from '../store/create-store.ts' import type { LiveStoreContextRunning as LiveStoreContextRunningType, Queryable } from '../store/store-types.ts' import type { Store as StoreClass } from '../store/store.ts' export const makeLiveStoreContext = ({ schema, storeId = 'default', context, boot, adapter, disableDevtools, onBootStatus, batchUpdates, syncPayload, syncPayloadSchema, }: LiveStoreContextProps): Effect.Effect< LiveStoreContextRunning['Type'], UnknownError | Cause.TimeoutException, DeferredStoreContext | Scope.Scope | OtelTracer.OtelTracer > => pipe( Effect.gen(function* () { const store = yield* createStore({ schema, storeId, adapter, batchUpdates, ...omitUndefineds({ context, boot, disableDevtools, onBootStatus, syncPayload, syncPayloadSchema }), }) return { stage: 'running', store } as any as LiveStoreContextRunning['Type'] }), Effect.tapErrorCause((cause) => Effect.flatMap(DeferredStoreContext, (def) => Deferred.failCause(def, cause))), Effect.tap((storeCtx) => Effect.flatMap(DeferredStoreContext, (def) => Deferred.succeed(def, storeCtx))), // This can take quite a while. // TODO make this configurable Effect.timeout(Duration.minutes(5)), Effect.withSpan('@livestore/livestore/effect:makeLiveStoreContext'), ) /** * @deprecated Use `Store.Tag(schema, storeId)` instead for type-safe store contexts. * * @example Migration * ```ts * // Before * const layer = LiveStoreContextLayer({ schema, adapter, ... }) * * // After * class MainStore extends Store.Tag(schema, 'main') {} * const layer = MainStore.layer({ adapter, ... }) * ``` */ export const LiveStoreContextLayer = ( props: LiveStoreContextProps, ): Layer.Layer => Layer.scoped(LiveStoreContextRunning, makeLiveStoreContext(props)).pipe( Layer.withSpan('LiveStore'), Layer.provide(LiveStoreContextDeferred), ) /** * @deprecated Use `Store.Tag(schema, storeId)` and `MainStore.DeferredLayer` instead. */ export const LiveStoreContextDeferred = Layer.effect( DeferredStoreContext, Deferred.make(), ) // ============================================================================= // Store.Tag - Idiomatic Effect API // ============================================================================= /** Branded type for unique store context identity */ declare const StoreContextTypeId: unique symbol /** Phantom type carrying schema and storeId information */ export interface StoreContextId { readonly [StoreContextTypeId]: { readonly schema: TSchema readonly storeId: TStoreId } } /** Phantom type for deferred store context */ declare const DeferredContextTypeId: unique symbol export interface DeferredContextId { readonly [DeferredContextTypeId]: { readonly storeId: TStoreId } } /** Props for creating a store layer (schema and storeId are already provided) */ export type StoreLayerProps = Omit< LiveStoreContextProps, 'storeId' | 'schema' > /** * Type for a Store.Tag class. This is the return type of `Store.Tag(schema, storeId)`. * Can be extended as a class and is yieldable in Effect.gen. * * Note: This uses a type alias with a new() signature to make it extendable. */ /** * Type for a Store.Tag class. Uses self-referential identifier so that `yield*` * and method R channels resolve to the same type, enabling proper layer composition. * * Uses `interface` (not `type`) to allow the self-referential identifier without * triggering TS2456 (circular type alias). */ export interface StoreTagClass extends Context.Tag, LiveStoreContextRunningType> { /** Constructor signature (makes the type extendable as a class) */ new (): Context.Tag, LiveStoreContextRunningType> /** Tag identity type (from Context.Tag) */ readonly Id: StoreTagClass /** Service type (from Context.Tag) */ readonly Type: LiveStoreContextRunningType /** The LiveStore schema for this store */ readonly schema: TSchema /** Unique identifier for this store */ readonly storeId: TStoreId /** Creates a layer that initializes the store */ layer( props: StoreLayerProps, ): Layer.Layer, UnknownError | Cause.TimeoutException, OtelTracer.OtelTracer> /** Deferred store tag for async initialization patterns */ readonly Deferred: Context.Tag< DeferredContextId, Deferred.Deferred, UnknownError> > /** Layer that provides the Deferred tag */ readonly DeferredLayer: Layer.Layer> /** Layer that waits for Deferred and provides the running store */ readonly fromDeferred: Layer.Layer, UnknownError, DeferredContextId> /** Query the store. Returns an Effect that yields the query result. */ query(query: Queryable): Effect.Effect> /** Commit events to the store. */ commit( ...eventInputs: LiveStoreEvent.Input.ForSchema[] ): Effect.Effect> /** Use the store with a callback function. */ use( f: (ctx: LiveStoreContextRunningType) => Effect.Effect, ): Effect.Effect> } /** * Create a typed store context class for use with Effect. * * Returns a class that extends `Context.Tag`, making it directly yieldable in Effect code. * The class includes static methods for creating layers and accessors for common operations. * * @param schema - The LiveStore schema (used for type inference and runtime) * @param storeId - Unique identifier for this store * * @example Basic usage * ```ts * import { Store } from '@livestore/livestore/effect' * import { schema } from './schema.ts' * * // Define your store (once per store) * export class MainStore extends Store.Tag(schema, 'main') {} * * // Create the layer * const storeLayer = MainStore.layer({ * adapter: myAdapter, * batchUpdates: ReactDOM.unstable_batchedUpdates, * }) * * // Use in Effect code * Effect.gen(function* () { * const { store } = yield* MainStore * // ^? Store - fully typed! * * // Or use accessors * const users = yield* MainStore.query(tables.users.all()) * yield* MainStore.commit(events.createUser({ id: '1', name: 'Alice' })) * }) * ``` * * @example Multiple stores * ```ts * class MainStore extends Store.Tag(mainSchema, 'main') {} * class SettingsStore extends Store.Tag(settingsSchema, 'settings') {} * * // Both available in same Effect context * Effect.gen(function* () { * const main = yield* MainStore * const settings = yield* SettingsStore * }) * * const layer = Layer.mergeAll( * MainStore.layer({ adapter: mainAdapter }), * SettingsStore.layer({ adapter: settingsAdapter }), * ) * ``` */ const makeStoreTag = ( schema: TSchema, storeId: TStoreId, ): StoreTagClass => { type RunningType = LiveStoreContextRunningType type DeferredType = Deferred.Deferred // Create the deferred tag and layers upfront const _DeferredTag = Context.GenericTag, DeferredType>( `@livestore/store-deferred/${storeId}`, ) const _DeferredLayer = Layer.effect(_DeferredTag, Deferred.make()) class Tag extends Context.Tag(`@livestore/store/${storeId}`)() { static readonly schema: TSchema = schema static readonly storeId: TStoreId = storeId static layer(props: StoreLayerProps) { return pipe( Effect.gen(function* () { const store = yield* createStore({ schema, storeId, adapter: props.adapter, batchUpdates: props.batchUpdates, ...omitUndefineds({ context: props.context, boot: props.boot, disableDevtools: props.disableDevtools, onBootStatus: props.onBootStatus, syncPayload: props.syncPayload, syncPayloadSchema: props.syncPayloadSchema, }), }) const ctx: RunningType = { stage: 'running', store: store as StoreClass } // Also fulfill the deferred if it exists in context yield* Effect.flatMap(Effect.serviceOption(_DeferredTag), (optDeferred) => optDeferred._tag === 'Some' ? Deferred.succeed(optDeferred.value, ctx) : Effect.void, ) return ctx }), Effect.timeout(Duration.minutes(5)), Effect.withSpan(`@livestore/effect:Store.Tag:${storeId}`), Layer.scoped(Tag), Layer.withSpan(`LiveStore:${storeId}`), Layer.provide(_DeferredLayer), ) } static readonly Deferred = _DeferredTag static readonly DeferredLayer = _DeferredLayer static readonly fromDeferred = pipe( Effect.gen(function* () { const deferred = yield* _DeferredTag const ctx = yield* deferred return Layer.succeed(Tag, ctx) }), Layer.unwrapScoped, ) static query(query: Queryable) { return Effect.map(Tag, ({ store }) => store.query(query)) } static commit(...eventInputs: LiveStoreEvent.Input.ForSchema[]) { return Effect.map(Tag, ({ store }) => { store.commit(...eventInputs) }) } static use(f: (ctx: RunningType) => Effect.Effect) { return Effect.flatMap(Tag, f) } } return Tag as unknown as StoreTagClass } /** * Store utilities for Effect integration. * * @example * ```ts * import { Store } from '@livestore/livestore/effect' * * export class MainStore extends Store.Tag(schema, 'main') {} * ``` */ export const Store = { /** * Create a typed store context class for use with Effect. * @see {@link makeStoreTag} for full documentation */ Tag: makeStoreTag, } // ============================================================================= // Legacy API (deprecated) // ============================================================================= /** * @deprecated Use `Store.Tag(schema, storeId)` instead. * * @example Migration * ```ts * // Before * const MainStoreContext = makeStoreContext()('main') * export const MainStore = MainStoreContext.Tag * export const MainStoreLayer = MainStoreContext.Layer * * // After * export class MainStore extends Store.Tag(schema, 'main') {} * // MainStore.layer({ ... }) for the layer * ``` */ export interface StoreContext { readonly storeId: TStoreId readonly Tag: Context.Tag, LiveStoreContextRunningType> readonly DeferredTag: Context.Tag< DeferredContextId, Deferred.Deferred, UnknownError> > readonly Layer: ( props: Omit, 'storeId'>, ) => Layer.Layer, UnknownError | Cause.TimeoutException, OtelTracer.OtelTracer> readonly DeferredLayer: Layer.Layer> readonly fromDeferred: Layer.Layer, UnknownError, DeferredContextId> } /** * @deprecated Use `Store.Tag(schema, storeId)` instead. * * @example Migration * ```ts * // Before * const MainStoreContext = makeStoreContext()('main') * * // After * class MainStore extends Store.Tag(schema, 'main') {} * ``` */ export const makeStoreContext = () => (storeId: TStoreId): StoreContext => { type RunningType = LiveStoreContextRunningType type DeferredType = Deferred.Deferred const Tag = Context.GenericTag, RunningType>(`@livestore/store/${storeId}`) const DeferredTag = Context.GenericTag, DeferredType>( `@livestore/store-deferred/${storeId}`, ) const DeferredLayer = Layer.effect(DeferredTag, Deferred.make()) const makeLayer = ( props: Omit, 'storeId'>, ): Layer.Layer, UnknownError | Cause.TimeoutException, OtelTracer.OtelTracer> => pipe( Effect.gen(function* () { const store = yield* createStore({ schema: props.schema, storeId, adapter: props.adapter, batchUpdates: props.batchUpdates, ...omitUndefineds({ context: props.context, boot: props.boot, disableDevtools: props.disableDevtools, onBootStatus: props.onBootStatus, syncPayload: props.syncPayload, syncPayloadSchema: props.syncPayloadSchema, }), }) const ctx: RunningType = { stage: 'running', store: store as StoreClass } // Also fulfill the deferred if it exists in context yield* Effect.flatMap(Effect.serviceOption(DeferredTag), (optDeferred) => optDeferred._tag === 'Some' ? Deferred.succeed(optDeferred.value, ctx) : Effect.void, ) return ctx }), Effect.timeout(Duration.minutes(5)), Effect.withSpan(`@livestore/effect:makeStoreContext:${storeId}`), Layer.scoped(Tag), Layer.withSpan(`LiveStore:${storeId}`), Layer.provide(DeferredLayer), ) const fromDeferred: Layer.Layer< StoreContextId, UnknownError, DeferredContextId > = pipe( Effect.gen(function* () { const deferred = yield* DeferredTag const ctx = yield* deferred return Layer.succeed(Tag, ctx) }), Layer.unwrapScoped, ) return { storeId, Tag, DeferredTag, Layer: makeLayer, DeferredLayer, fromDeferred, } }