@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.
- package/README.md +7 -6
- package/dist/index.cjs +111 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -8
- package/dist/index.d.ts +47 -8
- package/dist/index.js +110 -20
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
- package/src/components/KoraContextBridge.svelte +30 -0
- package/src/components/KoraStoreBridge.svelte +45 -0
- package/src/composables/use-app.ts +15 -0
- package/src/composables/use-collaborators.ts +24 -0
- package/src/composables/use-collection.ts +10 -0
- package/src/composables/use-mutation.ts +76 -0
- package/src/composables/use-presence.ts +32 -0
- package/src/composables/use-rich-text.ts +98 -0
- package/src/composables/use-sync-status.ts +30 -0
- package/src/context.ts +66 -0
- package/src/index.ts +34 -0
- package/src/stores/query-store.ts +186 -0
- package/src/types.ts +62 -0
|
@@ -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
|
+
}
|