@korajs/svelte 1.0.0-beta.11 → 1.0.0-beta.13

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.
@@ -0,0 +1,30 @@
1
+ import { OFFLINE_SYNC_STATUS, createSyncStatusController } from '@korajs/sync'
2
+ import type { SyncStatusInfo } from '@korajs/sync'
3
+ import { type Readable, readable } from 'svelte/store'
4
+ import { getKoraContext } from '../context'
5
+
6
+ /**
7
+ * Readable store of sync engine status.
8
+ */
9
+ export function createSyncStatusStore(): Readable<SyncStatusInfo> {
10
+ const { syncEngine, subscribeSyncStatus, events } = getKoraContext()
11
+
12
+ return readable<SyncStatusInfo>(OFFLINE_SYNC_STATUS, (set) => {
13
+ const controller = createSyncStatusController({
14
+ syncEngine,
15
+ subscribeSyncStatus,
16
+ events: subscribeSyncStatus ? null : events,
17
+ })
18
+ set(controller.getSnapshot())
19
+ const unsubscribe = controller.subscribe(() => {
20
+ set(controller.getSnapshot())
21
+ })
22
+ return () => {
23
+ unsubscribe()
24
+ controller.destroy()
25
+ }
26
+ })
27
+ }
28
+
29
+ /** Alias for {@link createSyncStatusStore}. */
30
+ export const useSyncStatus = createSyncStatusStore
package/src/context.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { KoraError } from '@korajs/core'
2
+ import { QueryStoreCache } from '@korajs/store'
3
+ import { getContext, setContext } from 'svelte'
4
+ import type { KoraAppLike, KoraContextValue } from './types'
5
+
6
+ // Registered (Symbol.for) keys: the .svelte components are compiled from src/ by the app
7
+ // (the "svelte" export condition) while helpers such as getCollection come from dist/, so
8
+ // two copies of this module exist in every app. Module-local Symbol() keys made the
9
+ // provider's context invisible to the helpers ("Kora context missing").
10
+ const koraContextKey = Symbol.for('@korajs/svelte:context')
11
+ const koraAppContextKey = Symbol.for('@korajs/svelte:app')
12
+
13
+ export function setKoraContext(value: KoraContextValue): void {
14
+ setContext(koraContextKey, value)
15
+ }
16
+
17
+ export function getKoraContext(): KoraContextValue {
18
+ const context = getContext<KoraContextValue | undefined>(koraContextKey)
19
+ if (!context) {
20
+ throw new KoraError(
21
+ 'Kora context missing. Wrap your app with <KoraProvider app={kora}> or <KoraStoreProvider store={store}>.',
22
+ 'KORA_NOT_PROVIDED',
23
+ { fix: 'Wrap your app with <KoraProvider app={kora}> or <KoraStoreProvider store={store}>.' },
24
+ )
25
+ }
26
+ return context
27
+ }
28
+
29
+ /** @deprecated Use {@link setKoraContext} via {@link KoraProvider} or {@link KoraStoreProvider}. */
30
+ export function setKoraAppContext(koraApp: KoraAppLike): void {
31
+ setContext(koraAppContextKey, koraApp)
32
+ }
33
+
34
+ /** @deprecated Use {@link getKoraContext} and {@link getApp}. */
35
+ export function getKoraApp(): KoraAppLike {
36
+ const app = getContext<KoraAppLike | undefined>(koraAppContextKey)
37
+ if (!app) {
38
+ throw new KoraError(
39
+ 'getKoraApp() requires setKoraAppContext(koraApp) on an ancestor.',
40
+ 'KORA_NOT_PROVIDED',
41
+ )
42
+ }
43
+ return app
44
+ }
45
+
46
+ /**
47
+ * Initialize Kora provider context after `app.ready`.
48
+ *
49
+ * @deprecated Prefer {@link KoraProvider} or {@link KoraStoreProvider} in root layout.
50
+ */
51
+ export async function initKoraProvider(app: KoraAppLike): Promise<KoraContextValue> {
52
+ await app.ready
53
+ const queryStoreCache =
54
+ typeof app.getQueryStoreCache === 'function' ? app.getQueryStoreCache() : new QueryStoreCache()
55
+ const value: KoraContextValue = {
56
+ store: app.getStore(),
57
+ syncEngine: app.getSyncEngine(),
58
+ app,
59
+ events: app.events ?? null,
60
+ subscribeSyncStatus: app.sync?.subscribeStatus ?? null,
61
+ queryStoreCache,
62
+ }
63
+ setKoraContext(value)
64
+ setKoraAppContext(app)
65
+ return value
66
+ }
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ export type {
2
+ KoraAppHandle,
3
+ KoraAppLike,
4
+ KoraContextValue,
5
+ UseMutationOptions,
6
+ UseMutationResult,
7
+ QueryState,
8
+ UseQueryOptions,
9
+ UseRichTextResult,
10
+ } from './types'
11
+
12
+ export {
13
+ getKoraApp,
14
+ getKoraContext,
15
+ initKoraProvider,
16
+ setKoraAppContext,
17
+ setKoraContext,
18
+ } from './context'
19
+
20
+ export {
21
+ createQueryStateStore,
22
+ createQueryStore,
23
+ useQuery,
24
+ useQueryState,
25
+ } from './stores/query-store'
26
+ export type { QueryInput } from './stores/query-store'
27
+ export { createMutation, useMutation } from './composables/use-mutation'
28
+ export { createSyncStatusStore, useSyncStatus } from './composables/use-sync-status'
29
+ export { getApp, useApp } from './composables/use-app'
30
+ export { getCollection, useCollection } from './composables/use-collection'
31
+ export { createRichTextBinding, useRichText } from './composables/use-rich-text'
32
+ export type { UseRichTextOptions } from './composables/use-rich-text'
33
+ export { applyPresence, usePresence } from './composables/use-presence'
34
+ export { createCollaboratorsStore, useCollaborators } from './composables/use-collaborators'
@@ -0,0 +1,186 @@
1
+ import type { CollectionRecord, QueryBuilder, QueryStore, QueryStoreCache } from '@korajs/store'
2
+ import { assertQueryReady, queryKey } from '@korajs/store'
3
+ import { type Readable, readable } from 'svelte/store'
4
+ import { getKoraContext } from '../context'
5
+ import type { QueryState, UseQueryOptions } from '../types'
6
+
7
+ const EMPTY_ARRAY: readonly unknown[] = Object.freeze([])
8
+
9
+ /** A query input: a builder, or a readable store of one (null disables). */
10
+ export type QueryInput<T> = QueryBuilder<T> | Readable<QueryBuilder<T> | null | undefined>
11
+
12
+ function isQueryBuilder<T>(input: QueryInput<T>): input is QueryBuilder<T> {
13
+ return typeof (input as { getDescriptor?: unknown }).getDescriptor === 'function'
14
+ }
15
+
16
+ function isReadable<V>(input: V | Readable<V>): input is Readable<V> {
17
+ return (
18
+ typeof input === 'object' &&
19
+ input !== null &&
20
+ typeof (input as { subscribe?: unknown }).subscribe === 'function'
21
+ )
22
+ }
23
+
24
+ /** The STORE-12 error channel of a QueryStore, read structurally (older store builds lack it). */
25
+ function readQueryError(queryStore: QueryStore<unknown>): Error | null {
26
+ const channel = queryStore as unknown as { getError?: () => Error | null }
27
+ return typeof channel.getError === 'function' ? channel.getError() : null
28
+ }
29
+
30
+ /**
31
+ * Follows the query and `enabled` inputs (values or readable stores), holding one
32
+ * ref-counted QueryStore per descriptor and switching when the descriptor changes
33
+ * (DX-7). Returns the teardown.
34
+ */
35
+ function followQuery<T>(
36
+ cache: QueryStoreCache,
37
+ query: QueryInput<T>,
38
+ options: UseQueryOptions | undefined,
39
+ emit: (state: QueryState<T>) => void,
40
+ ): () => void {
41
+ let current: QueryBuilder<T> | null = isQueryBuilder(query) ? query : null
42
+ const enabledInput = options?.enabled
43
+ let enabled = isReadable(enabledInput) ? true : enabledInput !== false
44
+ let activeKey: string | null = null
45
+ let release: (() => void) | null = null
46
+ let state: QueryState<T> = {
47
+ data: EMPTY_ARRAY as readonly T[],
48
+ error: null,
49
+ ready: false,
50
+ }
51
+
52
+ const publish = (next: QueryState<T>): void => {
53
+ if (next.data === state.data && next.error === state.error && next.ready === state.ready) {
54
+ return
55
+ }
56
+ if (next.error && next.error !== state.error) options?.onError?.(next.error)
57
+ state = next
58
+ emit(state)
59
+ }
60
+
61
+ const apply = (): void => {
62
+ const key = enabled && current ? queryKey(current.getDescriptor()) : null
63
+ if (key === activeKey) return
64
+ release?.()
65
+ release = null
66
+ activeKey = key
67
+ if (!key || !current) {
68
+ publish({ data: EMPTY_ARRAY as readonly T[], error: null, ready: false })
69
+ return
70
+ }
71
+ const builder = current
72
+ assertQueryReady(builder as QueryBuilder<unknown>)
73
+ const queryStore = cache.getOrCreate(builder)
74
+ const sync = (): void => {
75
+ const loaded = queryStore.hasSnapshot()
76
+ publish({
77
+ // A descriptor switch keeps the previous rows until the new query answers.
78
+ data: loaded ? queryStore.getSnapshot() : state.data,
79
+ error: readQueryError(queryStore as QueryStore<unknown>),
80
+ ready: loaded,
81
+ })
82
+ }
83
+ const unsubscribe = queryStore.subscribe(sync)
84
+ sync()
85
+ release = () => {
86
+ unsubscribe()
87
+ cache.release(builder as QueryBuilder<unknown>)
88
+ }
89
+ }
90
+
91
+ const stops: Array<() => void> = []
92
+ if (!isQueryBuilder(query)) {
93
+ stops.push(
94
+ query.subscribe((next) => {
95
+ current = next ?? null
96
+ apply()
97
+ }),
98
+ )
99
+ }
100
+ if (isReadable(enabledInput)) {
101
+ stops.push(
102
+ enabledInput.subscribe((next) => {
103
+ enabled = next !== false
104
+ apply()
105
+ }),
106
+ )
107
+ }
108
+ apply()
109
+
110
+ return () => {
111
+ for (const stop of stops) stop()
112
+ release?.()
113
+ release = null
114
+ activeKey = null
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Create a Svelte readable store of a Kora query's rows.
120
+ *
121
+ * The query and `enabled` may be plain values or readable stores: pass a store (for
122
+ * example a `derived` of your filter) and the rows follow it, re-subscribing when the
123
+ * query's descriptor changes. A failed query is reported through `options.onError`,
124
+ * or `console.error` without one; use {@link createQueryStateStore} to render it.
125
+ *
126
+ * @param query - A query builder, or a readable store of one (null disables)
127
+ * @param options - `enabled` (value or readable store; default true) and `onError`
128
+ * @returns A readable store of the current rows
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * const filter = writable(false)
133
+ * const todos = createQueryStore(derived(filter, (done) => app.todos.where({ completed: done })))
134
+ * ```
135
+ */
136
+ export function createQueryStore<T = CollectionRecord>(
137
+ query: QueryInput<T>,
138
+ options?: UseQueryOptions,
139
+ ): Readable<readonly T[]> {
140
+ const { queryStoreCache } = getKoraContext()
141
+ const reporting: UseQueryOptions = {
142
+ ...options,
143
+ onError:
144
+ options?.onError ??
145
+ ((failure: Error) => {
146
+ // Never swallowed: without a handler the failure is at least logged.
147
+ reportUnhandled(failure)
148
+ }),
149
+ }
150
+ return readable<readonly T[]>(EMPTY_ARRAY as readonly T[], (set) =>
151
+ followQuery(queryStoreCache, query, reporting, (state) => set(state.data)),
152
+ )
153
+ }
154
+
155
+ /**
156
+ * Like {@link createQueryStore}, but the store holds `{ data, error, ready }`.
157
+ *
158
+ * @param query - A query builder, or a readable store of one
159
+ * @param options - `enabled` and `onError`
160
+ * @returns A readable store of the query state
161
+ */
162
+ export function createQueryStateStore<T = CollectionRecord>(
163
+ query: QueryInput<T>,
164
+ options?: UseQueryOptions,
165
+ ): Readable<QueryState<T>> {
166
+ const { queryStoreCache } = getKoraContext()
167
+ const initial: QueryState<T> = { data: EMPTY_ARRAY as readonly T[], error: null, ready: false }
168
+ return readable<QueryState<T>>(initial, (set) =>
169
+ followQuery(queryStoreCache, query, options, set),
170
+ )
171
+ }
172
+
173
+ /**
174
+ * Logs a query failure nobody handles. Read through globalThis because this package
175
+ * builds without DOM or Node typings.
176
+ */
177
+ function reportUnhandled(failure: Error): void {
178
+ const host = globalThis as { console?: { error(...args: unknown[]): void } }
179
+ host.console?.error('[Kora] createQueryStore failed:', failure)
180
+ }
181
+
182
+ /** Alias for {@link createQueryStore}. */
183
+ export const useQuery = createQueryStore
184
+
185
+ /** Alias for {@link createQueryStateStore}. */
186
+ export const useQueryState = createQueryStateStore
package/src/types.ts ADDED
@@ -0,0 +1,62 @@
1
+ import type {
2
+ KoraAppLike as CoreKoraAppLike,
3
+ KoraContextValue as CoreKoraContextValue,
4
+ UseMutationOptions as CoreUseMutationOptions,
5
+ UseQueryOptions as CoreUseQueryOptions,
6
+ UseMutationResultBase,
7
+ } from '@korajs/core/bindings'
8
+ import type { QueryStoreCache, Store } from '@korajs/store'
9
+ import type { CursorInfo, SyncEngine, SyncStatusInfo } from '@korajs/sync'
10
+ import type { Readable } from 'svelte/store'
11
+ import type * as Y from 'yjs'
12
+
13
+ export type KoraAppLike = CoreKoraAppLike<Store, SyncEngine, QueryStoreCache>
14
+ export type KoraContextValue = CoreKoraContextValue<Store, SyncEngine, QueryStoreCache>
15
+ /** Options for `createQueryStore` and `createQueryStateStore`. */
16
+ export interface UseQueryOptions extends Omit<CoreUseQueryOptions, 'enabled'> {
17
+ /** When false, the query subscription is disabled. A value or a readable store. Defaults to true. */
18
+ enabled?: boolean | Readable<boolean>
19
+ /** Called when the query fails (STORE-12). `createQueryStore` logs to `console.error` without it. */
20
+ onError?: (error: Error) => void
21
+ }
22
+
23
+ /** Value of `createQueryStateStore`. */
24
+ export interface QueryState<T> {
25
+ /** The current rows (the last good rows while `error` is set). */
26
+ data: readonly T[]
27
+ /** The query's failure, or null. Cleared when results flow again. */
28
+ error: Error | null
29
+ /** False until the first result for the current query arrived. */
30
+ ready: boolean
31
+ }
32
+ export type UseMutationOptions<
33
+ TData,
34
+ TArgs extends unknown[],
35
+ TContext = void,
36
+ > = CoreUseMutationOptions<TData, TArgs, TContext>
37
+
38
+ export interface UseMutationResult<TData, TArgs extends unknown[]>
39
+ extends UseMutationResultBase<TData, TArgs> {
40
+ subscribeLoading: (fn: (value: boolean) => void) => () => void
41
+ subscribeError: (fn: (value: Error | null) => void) => () => void
42
+ readonly loading: boolean
43
+ readonly isLoading: boolean
44
+ readonly error: Error | null
45
+ }
46
+
47
+ /** @deprecated Use {@link KoraAppLike}. */
48
+ export type KoraAppHandle = KoraAppLike
49
+
50
+ export interface UseRichTextResult {
51
+ doc: Y.Doc
52
+ text: Y.Text
53
+ undo: () => void
54
+ redo: () => void
55
+ canUndo: boolean
56
+ canRedo: boolean
57
+ ready: boolean
58
+ error: Error | null
59
+ cursors: CursorInfo[]
60
+ setCursor: (anchor: number, head: number) => void
61
+ clearCursor: () => void
62
+ }