jquery-pro-query 0.0.0-stage β†’ 1.0.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 CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # jquery-pro-query 🌐 v1.1.0
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Smart Async Data Fetching, Automatic Caching, Request Deduplication, SWR, Optimistic UI, & Infinite Queries for jQuery.**
4
+
5
+ Bring modern server-state management (like TanStack/React Query v5 and SWR) to your legacy jQuery applications. Stop writing repetitive `$.ajax()` boilerplate and start building fast, cached, auto-updating web applications with zero backend changes.
6
+
7
+ ---
8
+
9
+ ## Features in v1.1.0 πŸš€
10
+
11
+ 1. **⚑ Optimistic UI Updates & Cache Rollback:** Instantly update the UI before the network request finishes, with automatic rollback if the server fails.
12
+ 2. **♾️ Infinite Scrolling & Paginated Queries (`$.infiniteQuery`):** Handle paginated feeds and "Load More" lists with `fetchNextPage()`, `hasNextPage`, and `isFetchingNextPage`.
13
+ 3. **πŸ’Ύ Offline Cache Persistence (`$.queryClient.setPersistence`):** Persist query cache to `localStorage` or `sessionStorage` so pages load instantly offline.
14
+ 4. **πŸ”— Conditional & Dependent Queries (`enabled` flag):** Execute queries conditionally (e.g. wait for a user ID signal before fetching user details).
15
+ 5. **πŸ”Œ Native `jquery-signals` Integration:** Reactive Signals for `data`, `isLoading`, `isFetching`, `isError`, `isFetchingNextPage`.
16
+
17
+ ---
18
+
19
+ ## Quick Example
20
+
21
+ ```javascript
22
+ // Enable Offline Cache Persistence
23
+ $.queryClient.setPersistence({ key: 'app_query_cache', storage: 'local' });
24
+
25
+ // 1. Conditional Query (only runs when userId has a value)
26
+ const userDetails = $.query(['user', userIdSignal], () => $.get(`/api/user/${userIdSignal.value}`), {
27
+ enabled: () => Boolean(userIdSignal.value)
28
+ });
29
+
30
+ // 2. Infinite Query for Paginated Lists
31
+ const auditLogs = $.infiniteQuery('audit-logs', ({ pageParam = 1 }) => $.get(`/api/logs?page=${pageParam}`), {
32
+ initialPageParam: 1,
33
+ getNextPageParam: (lastPage, allPages) => lastPage.hasMore ? allPages.length + 1 : undefined
34
+ });
35
+
36
+ // 3. Optimistic UI Mutation with Rollback
37
+ const updateName = $.mutation((newName) => $.post('/api/user', { name: newName }), {
38
+ onMutate: async (newName) => {
39
+ const previousData = $.queryClient.getQueryData('user');
40
+ $.queryClient.setQueryData('user', { ...previousData, name: newName }); // 0ms UI Update!
41
+ return { previousData }; // Return rollback context
42
+ },
43
+ onError: (err, variables, context) => {
44
+ if (context?.previousData) {
45
+ $.queryClient.setQueryData('user', context.previousData); // Automatic Rollback!
46
+ }
47
+ }
48
+ });
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Core API Reference
54
+
55
+ ### `$.query(queryKey, fetcherFn, [options])`
56
+ Creates a managed async query instance.
57
+
58
+ **Options:**
59
+ * `enabled` *(boolean|function)*: Conditional execution. If false or evaluates to false, query will not run.
60
+ * `staleTime` *(number)*: Time in ms before data is considered stale.
61
+ * `cacheTime` *(number)*: Time in ms data stays in memory cache (default: 5 mins).
62
+ * `refetchOnWindowFocus` *(boolean)*: Re-runs query when browser tab regains focus.
63
+ * `retry` *(number)*: Retry attempts on failure.
64
+
65
+ ---
66
+
67
+ ### `$.infiniteQuery(queryKey, fetcherFn, [options])`
68
+ Creates a managed paginated / infinite scrolling query instance.
69
+
70
+ **Options:**
71
+ * `initialPageParam`: Initial page parameter (default: `1`).
72
+ * `getNextPageParam(lastPage, allPages)`: Function determining the next page parameter or returning `undefined` if no more pages exist.
73
+
74
+ **Methods:**
75
+ * `fetchNextPage()`: Fetches the next page and appends it to `data.pages`.
76
+ * `hasNextPage`: Reactive signal/boolean indicating if more pages are available.
77
+ * `isFetchingNextPage`: Reactive signal/boolean indicating if a next page request is running.
78
+
79
+ ---
80
+
81
+ ### `$.mutation(mutationFn, [options])`
82
+ Creates a managed async mutation instance.
83
+
84
+ **Options:**
85
+ * `onMutate(variables)`: Fired before mutation starts (ideal for Optimistic UI updates). Returns rollback context.
86
+ * `onSuccess(data, variables, context)`: Fired when mutation succeeds.
87
+ * `onError(error, variables, context)`: Fired when mutation fails (ideal for cache rollbacks).
88
+ * `onSettled(data, error, variables, context)`: Fired on success or failure.
89
+
90
+ ---
91
+
92
+ ### `$.queryClient`
93
+ Global cache and query controller.
94
+
95
+ * `$.queryClient.setPersistence({ key, storage, maxAge })`: Enables persistent caching to `localStorage` / `sessionStorage`.
96
+ * `$.queryClient.setQueryData(key, updaterOrValue)`: Manually updates or populates cached query data.
97
+ * `$.queryClient.getQueryData(key)`: Retrieves cached data for a query key.
98
+ * `$.queryClient.invalidateQueries(keyPattern)`: Marks matching queries stale and triggers background refetch.
99
+ * `$.queryClient.prefetchQuery(key, fetcherFn)`: Fetches data ahead of time.
100
+
101
+ ---
102
+
103
+ ## License
104
+ MIT
package/example.html ADDED
@@ -0,0 +1,164 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>jquery-pro-query v1.1.0 Showcase</title>
7
+ <style>
8
+ body { font-family: system-ui, -apple-system, sans-serif; padding: 2rem; max-width: 700px; margin: 0 auto; line-height: 1.5; background: #f8fafc; color: #1e293b; }
9
+ .card { border: 1px solid #e2e8f0; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.04); margin-bottom: 1.5rem; background: #fff; }
10
+ button { background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.95rem; font-weight: 500; }
11
+ button:hover { background: #2563eb; }
12
+ button:disabled { background: #cbd5e1; cursor: not-allowed; }
13
+ button.btn-danger { background: #ef4444; }
14
+ button.btn-danger:hover { background: #dc2626; }
15
+ .status-tag { display: inline-block; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem; font-weight: 600; }
16
+ .bg-green { background: #dcfce7; color: #166534; }
17
+ .bg-blue { background: #dbeafe; color: #1e40af; }
18
+ .bg-amber { background: #fef3c7; color: #92400e; }
19
+ .hidden { display: none !important; }
20
+ ul { list-style: none; padding: 0; margin: 0.5rem 0 0 0; }
21
+ li { padding: 0.5rem; border-bottom: 1px solid #f1f5f9; }
22
+ </style>
23
+ </head>
24
+ <body>
25
+
26
+ <h1>🌐 jquery-pro-query v1.1.0 Showcase</h1>
27
+ <p style="color: #64748b; margin-top: -0.5rem; margin-bottom: 2rem;">Optimistic UI, Infinite Queries, Conditional Execution, & Offline Cache Persistence.</p>
28
+
29
+ <!-- Demo 1: Optimistic UI Updates & Automatic Rollback -->
30
+ <div class="card">
31
+ <h2>1. Optimistic UI Updates & Rollback</h2>
32
+ <p>Likes Count: <strong id="likes-count">0</strong> <span id="likes-status" class="status-tag bg-green">Synced</span></p>
33
+
34
+ <div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
35
+ <button id="btn-like-success">Like (Optimistic 0ms - Server Succeeds)</button>
36
+ <button id="btn-like-fail" class="btn-danger">Like (Optimistic 0ms - Server Fails & Rolls back)</button>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- Demo 2: Infinite Scrolling / Pagination Query -->
41
+ <div class="card">
42
+ <h2>2. Infinite Query (<code>$.infiniteQuery</code>)</h2>
43
+ <p>Loaded Logs:</p>
44
+ <ul id="logs-list"></ul>
45
+
46
+ <div style="margin-top: 1rem;">
47
+ <button id="btn-load-more">Load Next Page</button>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Demo 3: Conditional / Dependent Queries -->
52
+ <div class="card">
53
+ <h2>3. Conditional Query (<code>enabled</code>)</h2>
54
+ <p>Selected User ID: <strong id="selected-user-id">None</strong></p>
55
+ <p>Dependent User Data: <span id="user-details" style="font-weight: bold;">(Waiting for selection...)</span></p>
56
+
57
+ <div style="margin-top: 1rem; display: flex; gap: 0.5rem;">
58
+ <button id="btn-select-user-1">Select User #101</button>
59
+ <button id="btn-clear-user">Clear Selection</button>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- Load Dependencies -->
64
+ <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
65
+ <script src="../jquery-signals/src/jquery-signals.js"></script>
66
+ <script src="./src/jquery-pro-query.js"></script>
67
+
68
+ <script>
69
+ $(function() {
70
+ // Enable Offline Cache Persistence
71
+ $.queryClient.setPersistence({ key: 'demo_query_cache', storage: 'local' });
72
+
73
+ // --- Demo 1: Optimistic UI Setup ---
74
+ $.queryClient.setQueryData('post-likes', 42);
75
+
76
+ const likesQuery = $.query('post-likes', () => Promise.resolve($.queryClient.getQueryData('post-likes')));
77
+ $('#likes-count').bindSignal(likesQuery.dataSignal);
78
+
79
+ const likeMutation = $.mutation((shouldFail) => {
80
+ return new Promise((resolve, reject) => {
81
+ setTimeout(() => {
82
+ if (shouldFail) reject(new Error('Server Error: Network Disconnected'));
83
+ else resolve($.queryClient.getQueryData('post-likes'));
84
+ }, 1500);
85
+ });
86
+ }, {
87
+ onMutate: async (shouldFail) => {
88
+ // Snapshot previous data before optimistic update
89
+ const previousLikes = $.queryClient.getQueryData('post-likes');
90
+ // Optimistically increment UI to 0ms latency!
91
+ $.queryClient.setQueryData('post-likes', previousLikes + 1);
92
+ $('#likes-status').text('Optimistic Update (Server pending...)').removeClass('bg-green').addClass('bg-amber');
93
+
94
+ return { previousLikes }; // Return context for rollback
95
+ },
96
+ onSuccess: () => {
97
+ $('#likes-status').text('Synced with Server!').removeClass('bg-amber').addClass('bg-green');
98
+ },
99
+ onError: (err, variables, context) => {
100
+ // Automatic Rollback to snapshot!
101
+ if (context && context.previousLikes !== undefined) {
102
+ $.queryClient.setQueryData('post-likes', context.previousLikes);
103
+ }
104
+ alert(err.message + ' -> Rolled back to ' + context.previousLikes);
105
+ $('#likes-status').text('Rolled Back!').removeClass('bg-amber').addClass('bg-green');
106
+ }
107
+ });
108
+
109
+ $('#btn-like-success').on('click', () => likeMutation.mutate(false));
110
+ $('#btn-like-fail').on('click', () => likeMutation.mutate(true));
111
+
112
+ // --- Demo 2: Infinite Query Setup ---
113
+ const infiniteLogs = $.infiniteQuery('audit-logs', ({ pageParam = 1 }) => {
114
+ return new Promise((resolve) => {
115
+ setTimeout(() => {
116
+ resolve([
117
+ `Log Entry #${pageParam}-A: System boot`,
118
+ `Log Entry #${pageParam}-B: User login`,
119
+ `Log Entry #${pageParam}-C: Database backup`
120
+ ]);
121
+ }, 500);
122
+ });
123
+ }, {
124
+ initialPageParam: 1,
125
+ getNextPageParam: (lastPage, allPages) => allPages.length < 4 ? allPages.length + 1 : undefined
126
+ });
127
+
128
+ // Bind infinite pages array to list
129
+ $.effect(() => {
130
+ const pages = infiniteLogs.dataSignal.value?.pages || [];
131
+ const allItems = pages.flat();
132
+ $('#logs-list').empty();
133
+ allItems.forEach(item => $('#logs-list').append(`<li>${item}</li>`));
134
+ });
135
+
136
+ $('#btn-load-more').bindSignalProp('disabled', infiniteLogs.isFetchingNextPageSignal);
137
+ $('#btn-load-more').bindSignal(infiniteLogs.isFetchingNextPageSignal, fetching => fetching ? 'Loading Page...' : 'Load Next Page');
138
+ $('#btn-load-more').on('click', () => infiniteLogs.fetchNextPage());
139
+
140
+ // --- Demo 3: Conditional Query Setup ---
141
+ const selectedUserId = $.signal(null);
142
+ $('#selected-user-id').bindSignal(selectedUserId, id => id || 'None');
143
+
144
+ const dependentQuery = $.query(['user-detail', selectedUserId], () => {
145
+ return new Promise(resolve => {
146
+ setTimeout(() => {
147
+ resolve(`Details for User #${selectedUserId.value}: Role = Admin, Access = Granted`);
148
+ }, 800);
149
+ });
150
+ }, {
151
+ enabled: () => Boolean(selectedUserId.value) // Only runs when user ID is selected!
152
+ });
153
+
154
+ $('#user-details').bindSignal(dependentQuery.dataSignal, data => data || '(Waiting for selection...)');
155
+
156
+ $('#btn-select-user-1').on('click', () => selectedUserId.value = 101);
157
+ $('#btn-clear-user').on('click', () => {
158
+ selectedUserId.value = null;
159
+ $.queryClient.setQueryData(['user-detail', null], undefined);
160
+ });
161
+ });
162
+ </script>
163
+ </body>
164
+ </html>
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "jquery-pro-query",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Smart async data fetching, automatic caching, request deduplication, and SWR for jQuery.",
5
+ "main": "src/jquery-pro-query.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [
10
+ "jquery",
11
+ "jquery-plugin",
12
+ "query",
13
+ "cache",
14
+ "data-fetching",
15
+ "swr",
16
+ "stale-while-revalidate",
17
+ "mutation",
18
+ "ajax",
19
+ "optimistic-ui"
20
+ ],
21
+ "author": "prabhnoorsingh12",
22
+ "license": "MIT",
23
+ "peerDependencies": {
24
+ "jquery": ">=1.12.0"
25
+ }
26
+ }
@@ -0,0 +1,587 @@
1
+ /**
2
+ * jquery-pro-query v1.1.1
3
+ * Smart async data fetching, automatic caching, request deduplication, SWR, Optimistic UI, & Infinite Queries for jQuery.
4
+ * https://github.com/prabhnoorsingh12/jquery-pro-query
5
+ *
6
+ * (c) 2026 prabhnoorsingh12
7
+ * Released under the MIT License.
8
+ */
9
+
10
+ (function (factory) {
11
+ if (typeof define === 'function' && define.amd) {
12
+ // AMD
13
+ define(['jquery'], factory);
14
+ } else if (typeof exports === 'object') {
15
+ // Node/CommonJS
16
+ module.exports = factory(require('jquery'));
17
+ } else {
18
+ // Browser globals
19
+ if (typeof jQuery === 'undefined') {
20
+ throw new Error('jquery-pro-query requires jQuery');
21
+ }
22
+ factory(jQuery);
23
+ }
24
+ }(function ($) {
25
+ 'use strict';
26
+
27
+ // Helper to recursively unwrap Signals, getters, or functions inside Query Keys
28
+ function unwrapKey(key) {
29
+ if (key === null || key === undefined) return key;
30
+ if (typeof key === 'function') return unwrapKey(key());
31
+ if (typeof key === 'object' && typeof key.peek === 'function') return unwrapKey(key.peek());
32
+ if (typeof key === 'object' && key.value !== undefined && key.subscribers !== undefined) return unwrapKey(key.value);
33
+ if (Array.isArray(key)) return key.map(unwrapKey);
34
+ return key;
35
+ }
36
+
37
+ // Helper to normalize query keys (string or array) into a deterministic string identifier
38
+ function hashQueryKey(key) {
39
+ const unwrapped = unwrapKey(key);
40
+ if (typeof unwrapped === 'string') return unwrapped;
41
+ if (Array.isArray(unwrapped)) return JSON.stringify(unwrapped);
42
+ return String(unwrapped);
43
+ }
44
+
45
+ // Helper to create a reactive state property (uses $.signal if available, else standard getter/setter)
46
+ function createReactiveVal(initialVal) {
47
+ if (typeof $.signal === 'function') {
48
+ return $.signal(initialVal);
49
+ }
50
+ let _val = initialVal;
51
+ return {
52
+ get value() { return _val; },
53
+ set value(v) { _val = v; },
54
+ peek() { return _val; }
55
+ };
56
+ }
57
+
58
+ // Helper to convert jQuery $.Deferred / jqXHR or standard thenables safely to native Promise
59
+ function toNativePromise(thenableOrVal) {
60
+ if (thenableOrVal && typeof thenableOrVal.then === 'function') {
61
+ return new Promise((resolve, reject) => {
62
+ thenableOrVal.then(resolve, reject);
63
+ });
64
+ }
65
+ return Promise.resolve(thenableOrVal);
66
+ }
67
+
68
+ /**
69
+ * Query Class
70
+ */
71
+ class Query {
72
+ constructor(key, fetcherFn, options = {}) {
73
+ this.key = hashQueryKey(key);
74
+ this.rawKey = key;
75
+ this.fetcherFn = fetcherFn;
76
+
77
+ this.options = Object.assign({
78
+ enabled: true, // Conditional query execution
79
+ staleTime: 0, // Data is stale immediately by default
80
+ cacheTime: 300000, // 5 minutes in memory cache
81
+ refetchOnWindowFocus: true,
82
+ refetchOnReconnect: true,
83
+ retry: 3, // Retry attempts
84
+ retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), // Exponential backoff
85
+ refetchInterval: false // Polling interval
86
+ }, options);
87
+
88
+ // Reactive state signals
89
+ this._data = createReactiveVal(undefined);
90
+ this._isLoading = createReactiveVal(false);
91
+ this._isFetching = createReactiveVal(false);
92
+ this._isError = createReactiveVal(false);
93
+ this._error = createReactiveVal(null);
94
+ this._status = createReactiveVal('idle'); // 'idle' | 'loading' | 'success' | 'error'
95
+
96
+ this.updatedAt = 0;
97
+ this.inFlightPromise = null;
98
+ this.subscribers = new Set();
99
+ this.intervalId = null;
100
+ this.enabledDisposer = null;
101
+
102
+ // Auto-watch enabled flag if it's a function or signal
103
+ this.setupEnabledWatcher();
104
+
105
+ if (this.options.refetchInterval && typeof this.options.refetchInterval === 'number') {
106
+ this.startPolling();
107
+ }
108
+ }
109
+
110
+ get data() { return this._data.value !== undefined ? this._data.value : this._data; }
111
+ get isLoading() { return this._isLoading.value !== undefined ? this._isLoading.value : this._isLoading; }
112
+ get isFetching() { return this._isFetching.value !== undefined ? this._isFetching.value : this._isFetching; }
113
+ get isError() { return this._isError.value !== undefined ? this._isError.value : this._isError; }
114
+ get error() { return this._error.value !== undefined ? this._error.value : this._error; }
115
+
116
+ get dataSignal() { return this._data; }
117
+ get isLoadingSignal() { return this._isLoading; }
118
+ get isFetchingSignal() { return this._isFetching; }
119
+ get isErrorSignal() { return this._isError; }
120
+ get errorSignal() { return this._error; }
121
+
122
+ setupEnabledWatcher() {
123
+ if (typeof $.effect === 'function' && typeof this.options.enabled === 'function') {
124
+ this.enabledDisposer = $.effect(() => {
125
+ const isEnabled = this.isEnabled();
126
+ if (isEnabled && this.subscribers.size > 0 && this.isStale()) {
127
+ this.fetch();
128
+ }
129
+ });
130
+ }
131
+ }
132
+
133
+ subscribe(callback) {
134
+ const subId = Symbol();
135
+ this.subscribers.add(subId);
136
+ if (typeof callback === 'function') {
137
+ callback(this.data);
138
+ }
139
+ return () => {
140
+ this.subscribers.delete(subId);
141
+ if (this.subscribers.size === 0) {
142
+ this.stopPolling();
143
+ }
144
+ };
145
+ }
146
+
147
+ isEnabled() {
148
+ if (typeof this.options.enabled === 'function') {
149
+ return Boolean(this.options.enabled());
150
+ }
151
+ return this.options.enabled !== false;
152
+ }
153
+
154
+ isStale() {
155
+ if (this.updatedAt === 0) return true;
156
+ return (Date.now() - this.updatedAt) >= this.options.staleTime;
157
+ }
158
+
159
+ startPolling() {
160
+ this.stopPolling();
161
+ this.intervalId = setInterval(() => {
162
+ if (this.isEnabled() && this.subscribers.size > 0) {
163
+ this.fetch({ isBackground: true });
164
+ }
165
+ }, this.options.refetchInterval);
166
+ }
167
+
168
+ stopPolling() {
169
+ if (this.intervalId) {
170
+ clearInterval(this.intervalId);
171
+ this.intervalId = null;
172
+ }
173
+ }
174
+
175
+ setData(updaterOrValue) {
176
+ const current = this._data.value !== undefined ? this._data.value : (typeof this._data.peek === 'function' ? this._data.peek() : undefined);
177
+ const nextData = typeof updaterOrValue === 'function' ? updaterOrValue(current) : updaterOrValue;
178
+ this._data.value = nextData;
179
+ this._isError.value = false;
180
+ this._error.value = null;
181
+ this._status.value = 'success';
182
+ this.updatedAt = Date.now();
183
+ return nextData;
184
+ }
185
+
186
+ fetch(fetchOptions = {}) {
187
+ if (!this.isEnabled()) {
188
+ return Promise.resolve(this._data.value);
189
+ }
190
+
191
+ // 1. Request Deduplication
192
+ if (this.inFlightPromise) {
193
+ return this.inFlightPromise;
194
+ }
195
+
196
+ // 2. Cache Freshness Check
197
+ const hasCachedData = this._data.value !== undefined || (typeof this._data.peek === 'function' && this._data.peek() !== undefined);
198
+ if (hasCachedData && !this.isStale() && !fetchOptions.force) {
199
+ return Promise.resolve(this._data.value !== undefined ? this._data.value : this._data.peek());
200
+ }
201
+
202
+ if (!hasCachedData) {
203
+ this._isLoading.value = true;
204
+ this._status.value = 'loading';
205
+ }
206
+ this._isFetching.value = true;
207
+
208
+ let attempt = 0;
209
+
210
+ const executeFetchWithRetry = () => {
211
+ return toNativePromise(this.fetcherFn(unwrapKey(this.rawKey)))
212
+ .catch((err) => {
213
+ if (attempt < this.options.retry) {
214
+ attempt++;
215
+ const delay = typeof this.options.retryDelay === 'function'
216
+ ? this.options.retryDelay(attempt)
217
+ : this.options.retryDelay;
218
+ return new Promise((resolve) => setTimeout(resolve, delay)).then(executeFetchWithRetry);
219
+ }
220
+ throw err;
221
+ });
222
+ };
223
+
224
+ this.inFlightPromise = executeFetchWithRetry()
225
+ .then((result) => {
226
+ this.setData(result);
227
+ return result;
228
+ })
229
+ .catch((err) => {
230
+ this._isError.value = true;
231
+ this._error.value = err;
232
+ this._status.value = 'error';
233
+ throw err;
234
+ })
235
+ .finally(() => {
236
+ this._isLoading.value = false;
237
+ this._isFetching.value = false;
238
+ this.inFlightPromise = null;
239
+ });
240
+
241
+ return this.inFlightPromise;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * InfiniteQuery Class
247
+ */
248
+ class InfiniteQuery extends Query {
249
+ constructor(key, fetcherFn, options = {}) {
250
+ super(key, fetcherFn, Object.assign({
251
+ initialPageParam: 1,
252
+ getNextPageParam: () => undefined
253
+ }, options));
254
+
255
+ this._isFetchingNextPage = createReactiveVal(false);
256
+ this._hasNextPage = createReactiveVal(true);
257
+ this._data.value = { pages: [], pageParams: [] };
258
+ }
259
+
260
+ get isFetchingNextPage() { return this._isFetchingNextPage.value !== undefined ? this._isFetchingNextPage.value : this._isFetchingNextPage; }
261
+ get hasNextPage() { return this._hasNextPage.value !== undefined ? this._hasNextPage.value : this._hasNextPage; }
262
+ get isFetchingNextPageSignal() { return this._isFetchingNextPage; }
263
+ get hasNextPageSignal() { return this._hasNextPage; }
264
+
265
+ fetchNextPage() {
266
+ if (this.inFlightPromise || !this._hasNextPage.value) {
267
+ return Promise.resolve(this._data.value);
268
+ }
269
+
270
+ const currentData = this._data.value || { pages: [], pageParams: [] };
271
+ const lastPage = currentData.pages[currentData.pages.length - 1];
272
+ const nextPageParam = currentData.pages.length === 0
273
+ ? this.options.initialPageParam
274
+ : this.options.getNextPageParam(lastPage, currentData.pages);
275
+
276
+ if (nextPageParam === undefined) {
277
+ this._hasNextPage.value = false;
278
+ return Promise.resolve(currentData);
279
+ }
280
+
281
+ this._isFetchingNextPage.value = true;
282
+ this._isFetching.value = true;
283
+
284
+ this.inFlightPromise = toNativePromise(this.fetcherFn({ pageParam: nextPageParam, queryKey: unwrapKey(this.rawKey) }))
285
+ .then((pageData) => {
286
+ const newPages = [...currentData.pages, pageData];
287
+ const newPageParams = [...currentData.pageParams, nextPageParam];
288
+
289
+ this._data.value = { pages: newPages, pageParams: newPageParams };
290
+ this.updatedAt = Date.now();
291
+
292
+ const checkNext = this.options.getNextPageParam(pageData, newPages);
293
+ this._hasNextPage.value = checkNext !== undefined;
294
+
295
+ return this._data.value;
296
+ })
297
+ .catch((err) => {
298
+ this._isError.value = true;
299
+ this._error.value = err;
300
+ throw err;
301
+ })
302
+ .finally(() => {
303
+ this._isFetchingNextPage.value = false;
304
+ this._isFetching.value = false;
305
+ this.inFlightPromise = null;
306
+ });
307
+
308
+ return this.inFlightPromise;
309
+ }
310
+ }
311
+
312
+ /**
313
+ * QueryClient Class
314
+ */
315
+ class QueryClient {
316
+ constructor() {
317
+ this.queries = new Map();
318
+ this.persistenceOptions = null;
319
+ this.setupGlobalListeners();
320
+ }
321
+
322
+ setupGlobalListeners() {
323
+ if (typeof window !== 'undefined') {
324
+ window.addEventListener('focus', () => {
325
+ this.queries.forEach((query) => {
326
+ if (query.options.refetchOnWindowFocus && query.subscribers.size > 0 && query.isStale() && query.isEnabled()) {
327
+ query.fetch({ isBackground: true });
328
+ }
329
+ });
330
+ });
331
+
332
+ window.addEventListener('online', () => {
333
+ this.queries.forEach((query) => {
334
+ if (query.options.refetchOnReconnect && query.subscribers.size > 0 && query.isEnabled()) {
335
+ query.fetch({ isBackground: true });
336
+ }
337
+ });
338
+ });
339
+ }
340
+ }
341
+
342
+ getQuery(key, fetcherFn, options) {
343
+ const hashedKey = hashQueryKey(key);
344
+ let query = this.queries.get(hashedKey);
345
+ if (!query) {
346
+ query = new Query(key, fetcherFn, options);
347
+ this.queries.set(hashedKey, query);
348
+ } else if (options) {
349
+ Object.assign(query.options, options);
350
+ }
351
+ return query;
352
+ }
353
+
354
+ getInfiniteQuery(key, fetcherFn, options) {
355
+ const hashedKey = hashQueryKey(key);
356
+ let query = this.queries.get(hashedKey);
357
+ if (!query) {
358
+ query = new InfiniteQuery(key, fetcherFn, options);
359
+ this.queries.set(hashedKey, query);
360
+ } else if (options) {
361
+ Object.assign(query.options, options);
362
+ }
363
+ return query;
364
+ }
365
+
366
+ getQueryData(key) {
367
+ const hashedKey = hashQueryKey(key);
368
+ const query = this.queries.get(hashedKey);
369
+ if (!query) return undefined;
370
+ return query.data;
371
+ }
372
+
373
+ setQueryData(key, updaterOrValue) {
374
+ const hashedKey = hashQueryKey(key);
375
+ let query = this.queries.get(hashedKey);
376
+ if (!query) {
377
+ query = new Query(key, () => Promise.resolve(updaterOrValue));
378
+ this.queries.set(hashedKey, query);
379
+ }
380
+ const newNext = query.setData(updaterOrValue);
381
+ this.saveToPersistence();
382
+ return newNext;
383
+ }
384
+
385
+ invalidateQueries(keyPattern) {
386
+ const hashedPattern = hashQueryKey(keyPattern);
387
+ this.queries.forEach((query, key) => {
388
+ if (key === hashedPattern || key.startsWith(hashedPattern)) {
389
+ query.updatedAt = 0; // Mark stale
390
+ if (query.subscribers.size > 0 && query.isEnabled()) {
391
+ query.fetch({ force: true });
392
+ }
393
+ }
394
+ });
395
+ }
396
+
397
+ prefetchQuery(key, fetcherFn, options) {
398
+ const query = this.getQuery(key, fetcherFn, options);
399
+ return query.fetch();
400
+ }
401
+
402
+ clear() {
403
+ this.queries.forEach((query) => query.stopPolling());
404
+ this.queries.clear();
405
+ if (this.persistenceOptions) {
406
+ try {
407
+ const storage = this.persistenceOptions.storage === 'session' ? window.sessionStorage : window.localStorage;
408
+ storage.removeItem(this.persistenceOptions.key || 'jquery_pro_query_cache');
409
+ } catch (e) {}
410
+ }
411
+ }
412
+
413
+ setPersistence(options = {}) {
414
+ this.persistenceOptions = Object.assign({
415
+ key: 'jquery_pro_query_cache',
416
+ storage: 'local',
417
+ maxAge: 86400000 // 24 hours
418
+ }, options);
419
+
420
+ this.restoreFromPersistence();
421
+ }
422
+
423
+ saveToPersistence() {
424
+ if (!this.persistenceOptions) return;
425
+ try {
426
+ const storage = this.persistenceOptions.storage === 'session' ? window.sessionStorage : window.localStorage;
427
+ const serialized = {};
428
+
429
+ this.queries.forEach((query, key) => {
430
+ const dataVal = query._data.value !== undefined ? query._data.value : (typeof query._data.peek === 'function' ? query._data.peek() : undefined);
431
+ if (dataVal !== undefined) {
432
+ try {
433
+ // Test serializability before saving
434
+ JSON.stringify(dataVal);
435
+ serialized[key] = {
436
+ data: dataVal,
437
+ updatedAt: query.updatedAt,
438
+ rawKey: query.rawKey
439
+ };
440
+ } catch (err) {
441
+ // Ignore non-serializable DOM / Circular objects
442
+ }
443
+ }
444
+ });
445
+
446
+ storage.setItem(this.persistenceOptions.key, JSON.stringify(serialized));
447
+ } catch (e) {
448
+ console.warn('[jquery-pro-query] Failed to save cache persistence:', e);
449
+ }
450
+ }
451
+
452
+ restoreFromPersistence() {
453
+ if (!this.persistenceOptions) return;
454
+ try {
455
+ const storage = this.persistenceOptions.storage === 'session' ? window.sessionStorage : window.localStorage;
456
+ const raw = storage.getItem(this.persistenceOptions.key);
457
+ if (!raw) return;
458
+
459
+ const parsed = JSON.parse(raw);
460
+ const now = Date.now();
461
+
462
+ Object.keys(parsed).forEach((key) => {
463
+ const entry = parsed[key];
464
+ if ((now - entry.updatedAt) < this.persistenceOptions.maxAge) {
465
+ const query = this.getQuery(entry.rawKey || key);
466
+ query._data.value = entry.data;
467
+ query.updatedAt = entry.updatedAt;
468
+ query._isLoading.value = false;
469
+ query._status.value = 'success';
470
+ }
471
+ });
472
+ } catch (e) {
473
+ console.warn('[jquery-pro-query] Failed to restore cache persistence:', e);
474
+ }
475
+ }
476
+ }
477
+
478
+ const defaultClient = new QueryClient();
479
+
480
+ /**
481
+ * Mutation Class
482
+ */
483
+ class Mutation {
484
+ constructor(mutationFn, options = {}) {
485
+ this.mutationFn = mutationFn;
486
+ this.options = options;
487
+
488
+ this._data = createReactiveVal(undefined);
489
+ this._isLoading = createReactiveVal(false);
490
+ this._isError = createReactiveVal(false);
491
+ this._error = createReactiveVal(null);
492
+ }
493
+
494
+ get data() { return this._data.value !== undefined ? this._data.value : this._data; }
495
+ get isLoading() { return this._isLoading.value !== undefined ? this._isLoading.value : this._isLoading; }
496
+ get isError() { return this._isError.value !== undefined ? this._isError.value : this._isError; }
497
+ get error() { return this._error.value !== undefined ? this._error.value : this._error; }
498
+
499
+ async mutate(variables) {
500
+ this._isLoading.value = true;
501
+ this._isError.value = false;
502
+ this._error.value = null;
503
+
504
+ let context;
505
+
506
+ if (typeof this.options.onMutate === 'function') {
507
+ try {
508
+ context = await this.options.onMutate(variables);
509
+ } catch (e) {
510
+ console.error('[jquery-pro-query] Error in onMutate callback:', e);
511
+ }
512
+ }
513
+
514
+ try {
515
+ const result = await toNativePromise(this.mutationFn(variables));
516
+ this._data.value = result;
517
+
518
+ if (typeof this.options.onSuccess === 'function') {
519
+ await this.options.onSuccess(result, variables, context);
520
+ }
521
+
522
+ defaultClient.saveToPersistence();
523
+ return result;
524
+ } catch (err) {
525
+ this._isError.value = true;
526
+ this._error.value = err;
527
+
528
+ if (typeof this.options.onError === 'function') {
529
+ await this.options.onError(err, variables, context);
530
+ }
531
+
532
+ throw err;
533
+ } finally {
534
+ this._isLoading.value = false;
535
+ if (typeof this.options.onSettled === 'function') {
536
+ this.options.onSettled(this._data.value, this._error.value, variables, context);
537
+ }
538
+ }
539
+ }
540
+ }
541
+
542
+ // --- Attach Core API to jQuery Namespace ($) ---
543
+
544
+ $.queryClient = defaultClient;
545
+
546
+ /**
547
+ * $.query(key, fetcherFn, [options])
548
+ */
549
+ $.query = function (key, fetcherFn, options) {
550
+ const query = defaultClient.getQuery(key, fetcherFn, options);
551
+
552
+ // Subscribe and return un-subscribe disposer via query.unsubscribe
553
+ const unsubscribe = query.subscribe();
554
+ query.unsubscribe = unsubscribe;
555
+
556
+ if (query.isEnabled()) {
557
+ query.fetch();
558
+ }
559
+
560
+ return query;
561
+ };
562
+
563
+ /**
564
+ * $.infiniteQuery(key, fetcherFn, [options])
565
+ */
566
+ $.infiniteQuery = function (key, fetcherFn, options) {
567
+ const query = defaultClient.getInfiniteQuery(key, fetcherFn, options);
568
+
569
+ const unsubscribe = query.subscribe();
570
+ query.unsubscribe = unsubscribe;
571
+
572
+ if (query.isEnabled() && query._data.value.pages.length === 0) {
573
+ query.fetchNextPage();
574
+ }
575
+
576
+ return query;
577
+ };
578
+
579
+ /**
580
+ * $.mutation(mutationFn, [options])
581
+ */
582
+ $.mutation = function (mutationFn, options) {
583
+ return new Mutation(mutationFn, options);
584
+ };
585
+
586
+ return $;
587
+ }));