yaxa-svelte 2.5.0 → 2.7.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.
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export * from './client';
3
+ export * from './query-cache.svelte';
4
+ export * from './query.svelte';
5
+ export * from './mutation.svelte';
@@ -0,0 +1,37 @@
1
+ import type { ClientResponse, ProblemDetails } from './types';
2
+ export type MutationFn<TData = any, TVariables = any> = (variables: TVariables) => Promise<ClientResponse<TData> | TData>;
3
+ export interface CreateMutationOptions<TData = any, TVariables = any, TError = ProblemDetails | Error, TContext = unknown> {
4
+ /**
5
+ * Callback fired immediately before the mutation runs.
6
+ * Ideal for optimistic updates; returned context will be passed to onError and onSettled.
7
+ */
8
+ onMutate?: (variables: TVariables) => Promise<TContext> | TContext;
9
+ /**
10
+ * Callback fired when mutation succeeds.
11
+ */
12
+ onSuccess?: (data: TData, variables: TVariables, context: TContext | undefined) => Promise<void> | void;
13
+ /**
14
+ * Callback fired when mutation fails.
15
+ */
16
+ onError?: (error: TError, variables: TVariables, context: TContext | undefined) => Promise<void> | void;
17
+ /**
18
+ * Callback fired when mutation completes (either success or error).
19
+ */
20
+ onSettled?: (data: TData | null, error: TError | null, variables: TVariables, context: TContext | undefined) => Promise<void> | void;
21
+ }
22
+ export interface CreateMutationResult<TData = any, TVariables = any, TError = ProblemDetails | Error> {
23
+ readonly data: TData | null;
24
+ readonly error: TError | null;
25
+ readonly status: 'idle' | 'pending' | 'success' | 'error';
26
+ readonly isPending: boolean;
27
+ readonly isSuccess: boolean;
28
+ readonly isError: boolean;
29
+ readonly isIdle: boolean;
30
+ mutate: (variables: TVariables) => Promise<TData>;
31
+ reset: () => void;
32
+ }
33
+ /**
34
+ * Idiomatic Svelte 5 Runes Mutation Composable.
35
+ * Features optimistic rollbacks, lifecycle callbacks, and RFC 7807 problem details unwrapping.
36
+ */
37
+ export declare function createMutation<TData = any, TVariables = void, TError = ProblemDetails | Error, TContext = unknown>(mutationFn: MutationFn<TData, TVariables>, options?: CreateMutationOptions<TData, TVariables, TError, TContext>): CreateMutationResult<TData, TVariables, TError>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Checks if a response is a Yaxa ClientResponse.
3
+ */
4
+ function isClientResponse(res) {
5
+ return (typeof res === 'object' &&
6
+ res !== null &&
7
+ 'ok' in res &&
8
+ 'status' in res &&
9
+ ('data' in res || 'error' in res));
10
+ }
11
+ /**
12
+ * Idiomatic Svelte 5 Runes Mutation Composable.
13
+ * Features optimistic rollbacks, lifecycle callbacks, and RFC 7807 problem details unwrapping.
14
+ */
15
+ export function createMutation(mutationFn, options = {}) {
16
+ let data = $state(null);
17
+ let error = $state(null);
18
+ let status = $state('idle');
19
+ const isPending = $derived(status === 'pending');
20
+ const isSuccess = $derived(status === 'success');
21
+ const isError = $derived(status === 'error');
22
+ const isIdle = $derived(status === 'idle');
23
+ async function mutate(variables) {
24
+ status = 'pending';
25
+ error = null;
26
+ let context;
27
+ try {
28
+ if (options.onMutate) {
29
+ context = await options.onMutate(variables);
30
+ }
31
+ const result = await mutationFn(variables);
32
+ let finalData;
33
+ if (isClientResponse(result)) {
34
+ if (!result.ok) {
35
+ throw result.error;
36
+ }
37
+ finalData = result.data;
38
+ }
39
+ else {
40
+ finalData = result;
41
+ }
42
+ data = finalData;
43
+ status = 'success';
44
+ await options.onSuccess?.(finalData, variables, context);
45
+ await options.onSettled?.(finalData, null, variables, context);
46
+ return finalData;
47
+ }
48
+ catch (err) {
49
+ const errPayload = err;
50
+ error = errPayload;
51
+ status = 'error';
52
+ await options.onError?.(errPayload, variables, context);
53
+ await options.onSettled?.(null, errPayload, variables, context);
54
+ throw err;
55
+ }
56
+ }
57
+ function reset() {
58
+ data = null;
59
+ error = null;
60
+ status = 'idle';
61
+ }
62
+ return {
63
+ get data() {
64
+ return data;
65
+ },
66
+ get error() {
67
+ return error;
68
+ },
69
+ get status() {
70
+ return status;
71
+ },
72
+ get isPending() {
73
+ return isPending;
74
+ },
75
+ get isSuccess() {
76
+ return isSuccess;
77
+ },
78
+ get isError() {
79
+ return isError;
80
+ },
81
+ get isIdle() {
82
+ return isIdle;
83
+ },
84
+ mutate,
85
+ reset
86
+ };
87
+ }
@@ -0,0 +1,51 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
2
+ export type QueryKey = string | readonly unknown[];
3
+ /**
4
+ * Deterministically serializes a QueryKey into a cache string.
5
+ */
6
+ export declare function hashQueryKey(key: QueryKey): string;
7
+ export type QueryStatus = 'idle' | 'loading' | 'success' | 'error';
8
+ export interface QueryCacheEntry<TData = any, TError = any> {
9
+ readonly key: string;
10
+ data: TData | null;
11
+ error: TError | null;
12
+ status: QueryStatus;
13
+ isFetching: boolean;
14
+ updatedAt: number;
15
+ inFlightPromise: Promise<TData | null> | null;
16
+ subscribers: SvelteSet<() => void>;
17
+ invalidate: () => Promise<void>;
18
+ }
19
+ export interface InvalidateOptions {
20
+ key?: QueryKey;
21
+ exact?: boolean;
22
+ }
23
+ /**
24
+ * Runes-native Query Client managing cache entries, request coalescing,
25
+ * and real-time invalidation.
26
+ */
27
+ export declare class QueryClient {
28
+ private entries;
29
+ /**
30
+ * Retrieves or initializes a cache entry for a given key string.
31
+ */
32
+ getOrCreateEntry<TData = any, TError = any>(keyHash: string, initialData?: TData): QueryCacheEntry<TData, TError>;
33
+ /**
34
+ * Retrieves current data for a query key from the cache.
35
+ */
36
+ getQueryData<TData = any>(key: QueryKey): TData | null;
37
+ /**
38
+ * Manually sets or optimistically updates data for a query key.
39
+ */
40
+ setQueryData<TData = any>(key: QueryKey, updater: TData | ((previous: TData | null) => TData)): TData;
41
+ /**
42
+ * Invalidates queries matching a key or filter, triggering active subscribers to refetch.
43
+ */
44
+ invalidateQueries(filter?: InvalidateOptions | QueryKey): Promise<void>;
45
+ /**
46
+ * Clears the cache completely.
47
+ */
48
+ clear(): void;
49
+ }
50
+ export declare function getQueryClient(): QueryClient;
51
+ export declare function setQueryClient(client: QueryClient): void;
@@ -0,0 +1,172 @@
1
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
2
+ /**
3
+ * Deterministically serializes a QueryKey into a cache string.
4
+ */
5
+ export function hashQueryKey(key) {
6
+ if (typeof key === 'string')
7
+ return key;
8
+ try {
9
+ return JSON.stringify(key, (_, val) => {
10
+ if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
11
+ // Sort object keys deterministically
12
+ return Object.keys(val)
13
+ .sort()
14
+ .reduce((acc, k) => {
15
+ acc[k] = val[k];
16
+ return acc;
17
+ }, {});
18
+ }
19
+ return val;
20
+ });
21
+ }
22
+ catch {
23
+ return String(key);
24
+ }
25
+ }
26
+ /**
27
+ * Runes-native Query Client managing cache entries, request coalescing,
28
+ * and real-time invalidation.
29
+ */
30
+ export class QueryClient {
31
+ entries = new SvelteMap();
32
+ /**
33
+ * Retrieves or initializes a cache entry for a given key string.
34
+ */
35
+ getOrCreateEntry(keyHash, initialData) {
36
+ const entry = this.entries.get(keyHash);
37
+ if (!entry) {
38
+ let data = $state(initialData !== undefined ? initialData : null);
39
+ let error = $state(null);
40
+ let status = $state(initialData !== undefined ? 'success' : 'idle');
41
+ let isFetching = $state(false);
42
+ let updatedAt = $state(initialData !== undefined ? Date.now() : 0);
43
+ const subscribers = new SvelteSet();
44
+ const newEntry = {
45
+ key: keyHash,
46
+ get data() {
47
+ return data;
48
+ },
49
+ set data(val) {
50
+ data = val;
51
+ },
52
+ get error() {
53
+ return error;
54
+ },
55
+ set error(val) {
56
+ error = val;
57
+ },
58
+ get status() {
59
+ return status;
60
+ },
61
+ set status(val) {
62
+ status = val;
63
+ },
64
+ get isFetching() {
65
+ return isFetching;
66
+ },
67
+ set isFetching(val) {
68
+ isFetching = val;
69
+ },
70
+ get updatedAt() {
71
+ return updatedAt;
72
+ },
73
+ set updatedAt(val) {
74
+ updatedAt = val;
75
+ },
76
+ inFlightPromise: null,
77
+ subscribers,
78
+ invalidate: async () => {
79
+ for (const notify of Array.from(subscribers)) {
80
+ try {
81
+ notify();
82
+ }
83
+ catch {
84
+ // Ignored
85
+ }
86
+ }
87
+ }
88
+ };
89
+ this.entries.set(keyHash, newEntry);
90
+ return newEntry;
91
+ }
92
+ // If entry exists but has no data and initialData was provided, populate it
93
+ if (entry.data === null && initialData !== undefined && entry.status === 'idle') {
94
+ entry.data = initialData;
95
+ entry.status = 'success';
96
+ entry.updatedAt = Date.now();
97
+ }
98
+ return entry;
99
+ }
100
+ /**
101
+ * Retrieves current data for a query key from the cache.
102
+ */
103
+ getQueryData(key) {
104
+ const hash = hashQueryKey(key);
105
+ const entry = this.entries.get(hash);
106
+ return entry ? entry.data : null;
107
+ }
108
+ /**
109
+ * Manually sets or optimistically updates data for a query key.
110
+ */
111
+ setQueryData(key, updater) {
112
+ const hash = hashQueryKey(key);
113
+ const entry = this.getOrCreateEntry(hash);
114
+ const nextData = typeof updater === 'function'
115
+ ? updater(entry.data)
116
+ : updater;
117
+ entry.data = nextData;
118
+ entry.error = null;
119
+ entry.status = 'success';
120
+ entry.updatedAt = Date.now();
121
+ return nextData;
122
+ }
123
+ /**
124
+ * Invalidates queries matching a key or filter, triggering active subscribers to refetch.
125
+ */
126
+ async invalidateQueries(filter) {
127
+ if (!filter) {
128
+ for (const entry of this.entries.values()) {
129
+ await entry.invalidate();
130
+ }
131
+ return;
132
+ }
133
+ const isKey = typeof filter === 'string' || Array.isArray(filter);
134
+ const key = isKey ? filter : filter.key;
135
+ const exact = isKey ? false : Boolean(filter.exact);
136
+ if (!key) {
137
+ for (const entry of this.entries.values()) {
138
+ await entry.invalidate();
139
+ }
140
+ return;
141
+ }
142
+ const targetHash = hashQueryKey(key);
143
+ for (const [hash, entry] of this.entries.entries()) {
144
+ if (exact) {
145
+ if (hash === targetHash) {
146
+ await entry.invalidate();
147
+ }
148
+ }
149
+ else {
150
+ if (hash.startsWith(targetHash) || hash === targetHash) {
151
+ await entry.invalidate();
152
+ }
153
+ }
154
+ }
155
+ }
156
+ /**
157
+ * Clears the cache completely.
158
+ */
159
+ clear() {
160
+ this.entries.clear();
161
+ }
162
+ }
163
+ /**
164
+ * Global default query client singleton.
165
+ */
166
+ let globalClient = new QueryClient();
167
+ export function getQueryClient() {
168
+ return globalClient;
169
+ }
170
+ export function setQueryClient(client) {
171
+ globalClient = client;
172
+ }
@@ -0,0 +1,71 @@
1
+ import { type QueryClient, type QueryKey, type QueryStatus } from './query-cache.svelte';
2
+ import type { ClientResponse, ProblemDetails } from './types';
3
+ import type { SSEResult } from '../composables/useSSE.svelte';
4
+ export type QueryFn<TData = any> = () => Promise<ClientResponse<TData> | TData>;
5
+ export interface CreateQueryOptions<TData = any, TError = ProblemDetails | Error> {
6
+ /**
7
+ * Explicit cache key or reactive key getter. If omitted, an auto-derived key is used.
8
+ */
9
+ key?: QueryKey | (() => QueryKey);
10
+ /**
11
+ * Whether the query should execute automatically (defaults to true).
12
+ * Can be a boolean or a reactive boolean getter: `() => !!userId`.
13
+ */
14
+ enabled?: boolean | (() => boolean);
15
+ /**
16
+ * Initial data (useful for SSR hydration to avoid loading flashes).
17
+ */
18
+ initialData?: TData;
19
+ /**
20
+ * Time in milliseconds before data is considered stale (defaults to 0).
21
+ */
22
+ staleTimeMs?: number;
23
+ /**
24
+ * Whether to refetch data when the browser window regains focus (defaults to true).
25
+ */
26
+ refetchOnWindowFocus?: boolean;
27
+ /**
28
+ * Whether to refetch data when network reconnects (defaults to true).
29
+ */
30
+ refetchOnReconnect?: boolean;
31
+ /**
32
+ * Custom query client instance (defaults to global client).
33
+ */
34
+ client?: QueryClient;
35
+ /**
36
+ * Server-Sent Events stream or channel for live real-time cache invalidation.
37
+ */
38
+ subscribeToSse?: string | {
39
+ sse?: SSEResult;
40
+ event?: string;
41
+ };
42
+ /**
43
+ * Callback on successful fetch.
44
+ */
45
+ onSuccess?: (data: TData) => void;
46
+ /**
47
+ * Callback on error.
48
+ */
49
+ onError?: (error: TError) => void;
50
+ }
51
+ export interface CreateQueryResult<TData = any, TError = ProblemDetails | Error> {
52
+ readonly data: TData | null;
53
+ readonly error: TError | null;
54
+ readonly status: QueryStatus;
55
+ readonly isLoading: boolean;
56
+ readonly isFetching: boolean;
57
+ readonly isSuccess: boolean;
58
+ readonly isError: boolean;
59
+ readonly updatedAt: number;
60
+ refetch: () => Promise<TData | null>;
61
+ invalidate: () => Promise<void>;
62
+ setData: (updater: TData | ((prev: TData | null) => TData)) => void;
63
+ optimisticUpdate: (updater: (prev: TData | null) => TData) => void;
64
+ destroy: () => void;
65
+ }
66
+ /**
67
+ * Idiomatic Svelte 5 Runes Query Composable.
68
+ * Features automatic dependency tracking, request coalescing, stale-while-revalidate,
69
+ * window focus revalidation, and SSE live invalidation.
70
+ */
71
+ export declare function createQuery<TData = any, TError = ProblemDetails | Error>(keyOrFn: QueryKey | (() => QueryKey) | QueryFn<TData>, fnOrOptions?: QueryFn<TData> | CreateQueryOptions<TData, TError>, maybeOptions?: CreateQueryOptions<TData, TError>): CreateQueryResult<TData, TError>;
@@ -0,0 +1,213 @@
1
+ import { hashQueryKey, getQueryClient } from './query-cache.svelte';
2
+ /**
3
+ * Checks if a response is a Yaxa ClientResponse.
4
+ */
5
+ function isClientResponse(res) {
6
+ return (typeof res === 'object' &&
7
+ res !== null &&
8
+ 'ok' in res &&
9
+ 'status' in res &&
10
+ ('data' in res || 'error' in res));
11
+ }
12
+ /**
13
+ * Normalizes query key evaluation.
14
+ */
15
+ function resolveKey(keyOrFn, fallback) {
16
+ if (typeof keyOrFn === 'function') {
17
+ return keyOrFn();
18
+ }
19
+ if (keyOrFn !== undefined) {
20
+ return keyOrFn;
21
+ }
22
+ return fallback || 'default-query-key';
23
+ }
24
+ /**
25
+ * Idiomatic Svelte 5 Runes Query Composable.
26
+ * Features automatic dependency tracking, request coalescing, stale-while-revalidate,
27
+ * window focus revalidation, and SSE live invalidation.
28
+ */
29
+ export function createQuery(keyOrFn, fnOrOptions, maybeOptions) {
30
+ let keyConfig;
31
+ let queryFn;
32
+ let options;
33
+ if (typeof keyOrFn === 'function' && fnOrOptions && typeof fnOrOptions === 'function') {
34
+ // createQuery(keyFn, queryFn, options)
35
+ keyConfig = keyOrFn;
36
+ queryFn = fnOrOptions;
37
+ options = maybeOptions || {};
38
+ }
39
+ else if (typeof keyOrFn === 'function' && (!fnOrOptions || typeof fnOrOptions === 'object')) {
40
+ // createQuery(queryFn, options)
41
+ queryFn = keyOrFn;
42
+ options = fnOrOptions || {};
43
+ keyConfig = options.key;
44
+ }
45
+ else {
46
+ // createQuery(key, queryFn, options)
47
+ keyConfig = keyOrFn;
48
+ queryFn = fnOrOptions;
49
+ options = maybeOptions || {};
50
+ }
51
+ const client = options.client || getQueryClient();
52
+ const staleTimeMs = options.staleTimeMs ?? 0;
53
+ const refetchOnWindowFocus = options.refetchOnWindowFocus ?? true;
54
+ const refetchOnReconnect = options.refetchOnReconnect ?? true;
55
+ const fallbackKey = queryFn.toString();
56
+ const initialKey = resolveKey(keyConfig, fallbackKey);
57
+ const initialHash = hashQueryKey(initialKey);
58
+ // Pre-seed entry immediately on creation with initialData if provided
59
+ client.getOrCreateEntry(initialHash, options.initialData);
60
+ function getCurrentKey() {
61
+ return resolveKey(keyConfig, fallbackKey);
62
+ }
63
+ function getCurrentHash() {
64
+ return hashQueryKey(getCurrentKey());
65
+ }
66
+ function getIsEnabled() {
67
+ return typeof options.enabled === 'function'
68
+ ? options.enabled()
69
+ : options.enabled !== undefined
70
+ ? options.enabled
71
+ : true;
72
+ }
73
+ function getEntry() {
74
+ return client.getOrCreateEntry(getCurrentHash(), options.initialData);
75
+ }
76
+ async function executeFetch(force = false) {
77
+ if (!getIsEnabled())
78
+ return null;
79
+ const targetEntry = getEntry();
80
+ // 1. Check Stale Time if not forced
81
+ if (!force && targetEntry.updatedAt > 0) {
82
+ const age = Date.now() - targetEntry.updatedAt;
83
+ if (age < staleTimeMs && targetEntry.data !== null) {
84
+ return targetEntry.data;
85
+ }
86
+ }
87
+ // 2. Request Coalescing: Reuse in-flight Promise if already running
88
+ if (targetEntry.inFlightPromise) {
89
+ return targetEntry.inFlightPromise;
90
+ }
91
+ targetEntry.isFetching = true;
92
+ if (targetEntry.status === 'idle') {
93
+ targetEntry.status = 'loading';
94
+ }
95
+ const promise = (async () => {
96
+ try {
97
+ const result = await queryFn();
98
+ let finalData;
99
+ if (isClientResponse(result)) {
100
+ if (!result.ok) {
101
+ throw result.error;
102
+ }
103
+ finalData = result.data;
104
+ }
105
+ else {
106
+ finalData = result;
107
+ }
108
+ targetEntry.data = finalData;
109
+ targetEntry.error = null;
110
+ targetEntry.status = 'success';
111
+ targetEntry.updatedAt = Date.now();
112
+ options.onSuccess?.(finalData);
113
+ return finalData;
114
+ }
115
+ catch (err) {
116
+ targetEntry.error = err;
117
+ targetEntry.status = 'error';
118
+ options.onError?.(err);
119
+ return null;
120
+ }
121
+ finally {
122
+ targetEntry.isFetching = false;
123
+ targetEntry.inFlightPromise = null;
124
+ }
125
+ })();
126
+ targetEntry.inFlightPromise = promise;
127
+ return promise;
128
+ }
129
+ // Subscriber notification callback
130
+ const subscriber = () => {
131
+ executeFetch(true);
132
+ };
133
+ const initialEntry = getEntry();
134
+ initialEntry.subscribers.add(subscriber);
135
+ // Initial execution if enabled and not already hydrated/fresh
136
+ if (getIsEnabled() && initialEntry.status === 'idle') {
137
+ executeFetch(false);
138
+ }
139
+ // Window listeners
140
+ let cleanupWindowListeners;
141
+ if (typeof window !== 'undefined') {
142
+ const onFocus = () => {
143
+ if (refetchOnWindowFocus && getIsEnabled()) {
144
+ executeFetch(false);
145
+ }
146
+ };
147
+ const onOnline = () => {
148
+ if (refetchOnReconnect && getIsEnabled()) {
149
+ executeFetch(true);
150
+ }
151
+ };
152
+ window.addEventListener('focus', onFocus);
153
+ window.addEventListener('online', onOnline);
154
+ cleanupWindowListeners = () => {
155
+ window.removeEventListener('focus', onFocus);
156
+ window.removeEventListener('online', onOnline);
157
+ };
158
+ }
159
+ // SSE Live-Invalidation Bridge
160
+ let cleanupSse;
161
+ if (options.subscribeToSse && typeof options.subscribeToSse === 'object') {
162
+ const sseOption = options.subscribeToSse;
163
+ if (sseOption.sse && sseOption.event) {
164
+ cleanupSse = sseOption.sse.on(sseOption.event, () => {
165
+ executeFetch(true);
166
+ });
167
+ }
168
+ }
169
+ function destroy() {
170
+ const currentEntry = getEntry();
171
+ currentEntry.subscribers.delete(subscriber);
172
+ cleanupWindowListeners?.();
173
+ cleanupSse?.();
174
+ }
175
+ return {
176
+ get data() {
177
+ return getEntry().data;
178
+ },
179
+ get error() {
180
+ return getEntry().error;
181
+ },
182
+ get status() {
183
+ return getEntry().status;
184
+ },
185
+ get isLoading() {
186
+ const e = getEntry();
187
+ return e.status === 'loading' && e.data === null;
188
+ },
189
+ get isFetching() {
190
+ return getEntry().isFetching;
191
+ },
192
+ get isSuccess() {
193
+ return getEntry().status === 'success';
194
+ },
195
+ get isError() {
196
+ return getEntry().status === 'error';
197
+ },
198
+ get updatedAt() {
199
+ return getEntry().updatedAt;
200
+ },
201
+ refetch: () => executeFetch(true),
202
+ invalidate: async () => {
203
+ await getEntry().invalidate();
204
+ },
205
+ setData: (updater) => {
206
+ client.setQueryData(getCurrentKey(), updater);
207
+ },
208
+ optimisticUpdate: (updater) => {
209
+ client.setQueryData(getCurrentKey(), updater);
210
+ },
211
+ destroy
212
+ };
213
+ }
@@ -0,0 +1 @@
1
+ export {};