@raf-query/core 0.0.18 → 0.1.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/dist/observer.mjs CHANGED
@@ -1,4 +1,43 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import { Platform } from "./platform.mjs";
3
+ import { unsafeCast } from "./cast.mjs";
4
+ import { listen } from "./listen.mjs";
5
+ import { entryOf } from "./entry.mjs";
6
+ import { fetchNextPage, fetchPreviousPage, getUnsafe as getUnsafe$1, hasNextPageUnsafe, hasPreviousPageUnsafe, refetch, subscribeUnsafe as subscribeUnsafe$1 } from "./query.mjs";
7
+ import { Effect, Exit, FiberHandle, Pipeable, Predicate, Scope } from "effect";
1
8
  //#region src/observer.ts
9
+ /**
10
+ * One reader's view of one query at a time, as a data type, like TanStack
11
+ * Query's `QueryObserver`: it derives the reader's result from the query's
12
+ * state, keeps a previous query's data for `whilePending: "previous"`, runs
13
+ * `refetchInterval`, and notifies its listeners only when a result property
14
+ * they read changed (see {@link trackResult}). Bindings make one per hook.
15
+ *
16
+ * The `*Unsafe` functions read and update synchronously, for renders and
17
+ * external stores. While it has listeners, the observer holds a `Scope` (a
18
+ * child of its query's) that owns the query subscription and the polling
19
+ * fiber; the last listener leaving, a switch to another query, or the query
20
+ * leaving its cache closes it.
21
+ */
22
+ var observer_exports = /* @__PURE__ */ __exportAll({
23
+ TypeId: () => TypeId,
24
+ changes: () => changes,
25
+ get: () => get,
26
+ getOptimisticResultUnsafe: () => getOptimisticResultUnsafe,
27
+ getUnsafe: () => getUnsafe,
28
+ isQueryObserver: () => isQueryObserver,
29
+ make: () => make,
30
+ makeUnsafe: () => makeUnsafe,
31
+ setOptionsUnsafe: () => setOptionsUnsafe,
32
+ subscribeUnsafe: () => subscribeUnsafe,
33
+ trackProp: () => trackProp,
34
+ trackResult: () => trackResult
35
+ });
36
+ const TypeId = "~@raf-query/core/QueryObserver";
37
+ const Proto = {
38
+ ...Pipeable.Prototype,
39
+ [TypeId]: { _A: (value) => value }
40
+ };
2
41
  const resultKeys = {
3
42
  error: true,
4
43
  fetchNextPage: true,
@@ -18,150 +57,193 @@ const resultKeys = {
18
57
  refetch: true,
19
58
  updatedAt: true
20
59
  };
60
+ function isQueryObserver(value) {
61
+ return Predicate.hasProperty(value, TypeId);
62
+ }
63
+ function implOf(self) {
64
+ return unsafeCast(self);
65
+ }
21
66
  function isResultKey(name) {
22
- return typeof name === "string" && Object.hasOwn(resultKeys, name);
67
+ return Predicate.isString(name) && Object.hasOwn(resultKeys, name);
23
68
  }
24
69
  function shallowEqual(a, b) {
25
70
  for (const name of Object.keys(resultKeys)) if (isResultKey(name) && a[name] !== b[name]) return false;
26
71
  return true;
27
72
  }
73
+ /** The result for `query` with `options`, given the last query that had data. */
74
+ function resultOf(query, options, lastWithData) {
75
+ const state = getUnsafe$1(query);
76
+ const previous = options.whilePending === "previous" && !state.hasData && state.status !== "error" && lastWithData !== void 0 && lastWithData.hash !== query.hash ? lastWithData : void 0;
77
+ const shown = previous ?? query;
78
+ const shownState = previous ? getUnsafe$1(previous) : state;
79
+ return {
80
+ error: state.error,
81
+ fetchNextPage: fetchNextPage(query),
82
+ fetchPreviousPage: fetchPreviousPage(query),
83
+ hash: query.hash,
84
+ hasNextPage: hasNextPageUnsafe(query),
85
+ hasPreviousPage: hasPreviousPageUnsafe(query),
86
+ input: shown.handle.input,
87
+ isFetching: state.isFetching,
88
+ isFetchingNextPage: state.fetchDirection === "forward",
89
+ isFetchingPreviousPage: state.fetchDirection === "backward",
90
+ isPending: !state.hasData,
91
+ isPrevious: previous !== void 0,
92
+ isStale: state.isStale,
93
+ key: shown.handle.key,
94
+ promise: shownState.promise,
95
+ refetch: refetch(query),
96
+ updatedAt: shownState.updatedAt
97
+ };
98
+ }
99
+ /** Observe `query`. Doesn't subscribe to it until the observer has a listener. */
100
+ function makeUnsafe(query, options = {}) {
101
+ return Object.assign(Object.create(Proto), {
102
+ current: resultOf(query, options, void 0),
103
+ lastWithData: getUnsafe$1(query).hasData ? query : void 0,
104
+ listeners: /* @__PURE__ */ new Set(),
105
+ options,
106
+ query,
107
+ subscription: void 0,
108
+ trackedProps: /* @__PURE__ */ new Set()
109
+ });
110
+ }
111
+ /** {@link makeUnsafe}, as an Effect. */
112
+ function make(query, options) {
113
+ return Effect.sync(() => makeUnsafe(query, options));
114
+ }
115
+ /** The current result. */
116
+ function getUnsafe(self) {
117
+ return implOf(self).current;
118
+ }
119
+ /** {@link getUnsafe}, as an Effect. */
120
+ function get(self) {
121
+ return Effect.sync(() => getUnsafe(self));
122
+ }
28
123
  /**
29
- * One reader of one query at a time, like TanStack Query's `QueryObserver`:
30
- * it derives the reader's result from the query's state, keeps a previous
31
- * query's data for `whilePending: "previous"`, runs `refetchInterval`, and
32
- * notifies its listeners only when a result property they read changed
33
- * (see {@link QueryObserver.trackResult}). Bindings create one per hook.
124
+ * The result for `query` with `options` right now, without switching to
125
+ * them. Bindings call this during render, before `setOptionsUnsafe` commits.
34
126
  */
35
- var QueryObserver = class {
36
- #current;
37
- #interval;
38
- #lastWithData;
39
- #listeners = /* @__PURE__ */ new Set();
40
- #options;
41
- #query;
42
- #trackedProps = /* @__PURE__ */ new Set();
43
- #unsubscribe;
44
- constructor(query, options = {}) {
45
- this.#query = query;
46
- this.#options = options;
47
- this.#current = this.#createResult(query, options);
48
- this.updateResult();
49
- }
50
- /**
51
- * The result for `query` with `options` right now, without switching to
52
- * it. Bindings call this during render, before `setOptions` commits.
53
- */
54
- getOptimisticResult(query, options) {
55
- const result = this.#createResult(query, options);
56
- if (!shallowEqual(this.#current, result)) this.#current = result;
57
- return result;
127
+ function getOptimisticResultUnsafe(self, query, options) {
128
+ const impl = implOf(self);
129
+ const result = resultOf(query, options, impl.lastWithData);
130
+ if (!shallowEqual(impl.current, result)) impl.current = result;
131
+ return result;
132
+ }
133
+ /** Observe `query` with `options` from now on. */
134
+ function setOptionsUnsafe(self, query, options) {
135
+ const impl = implOf(self);
136
+ const previous = impl.query;
137
+ const previousInterval = impl.options.refetchInterval;
138
+ impl.query = query;
139
+ impl.options = options;
140
+ if (impl.listeners.size > 0) {
141
+ if (query !== previous) connect(impl);
142
+ else if (options.refetchInterval !== previousInterval) restartPolling(impl);
58
143
  }
59
- getCurrentResult = () => this.#current;
60
- /** Observe `query` with `options` from now on. */
61
- setOptions(query, options) {
62
- const previous = this.#query;
63
- const previousInterval = this.#options.refetchInterval;
64
- this.#query = query;
65
- this.#options = options;
66
- if (this.#listeners.size > 0) {
67
- if (query !== previous) {
68
- this.#unsubscribe?.();
69
- this.#unsubscribe = this.#subscribeToQuery();
70
- }
71
- if (query !== previous || options.refetchInterval !== previousInterval) this.#startInterval();
72
- }
73
- this.updateResult();
144
+ updateResult(impl);
145
+ }
146
+ /** Listen for result changes. The first listener subscribes to the query. */
147
+ function subscribeUnsafe(self, listener) {
148
+ const impl = implOf(self);
149
+ impl.listeners.add(listener);
150
+ if (!impl.subscription) {
151
+ connect(impl);
152
+ updateResult(impl);
74
153
  }
75
- /** Listen for result changes. The first listener subscribes to the query. */
76
- subscribe = (listener) => {
77
- this.#listeners.add(listener);
78
- if (this.#listeners.size === 1) {
79
- this.#unsubscribe = this.#subscribeToQuery();
80
- this.#startInterval();
81
- this.updateResult();
82
- }
83
- return () => {
84
- if (!this.#listeners.delete(listener) || this.#listeners.size > 0) return;
85
- this.#unsubscribe?.();
86
- this.#unsubscribe = void 0;
87
- clearInterval(this.#interval);
88
- };
154
+ return () => {
155
+ if (!impl.listeners.delete(listener) || impl.listeners.size > 0) return;
156
+ disconnect(impl);
89
157
  };
90
- /**
91
- * Wrap `result` so that reading a property marks it tracked. Once any
92
- * property is tracked, listeners are only notified when a tracked one
93
- * changes.
94
- */
95
- trackResult(result) {
96
- return new Proxy(result, { get: (target, name, receiver) => {
97
- if (isResultKey(name)) this.trackProp(name);
98
- return Reflect.get(target, name, receiver);
99
- } });
100
- }
101
- /** Mark `name` as read, for bindings that track access themselves. */
102
- trackProp(name) {
103
- this.#trackedProps.add(name);
104
- }
105
- /** Recompute the result, and notify listeners if a tracked property changed. */
106
- updateResult() {
107
- const previous = this.#current;
108
- const next = this.#createResult(this.#query, this.#options);
109
- if (this.#query.getState().hasData) this.#lastWithData = this.#query;
110
- if (shallowEqual(previous, next)) return;
111
- this.#current = next;
112
- if (this.#shouldNotify(previous, next)) for (const listener of this.#listeners) listener();
113
- }
114
- #shouldNotify(previous, next) {
115
- if (this.#trackedProps.size === 0) return true;
116
- for (const name of this.#trackedProps) if (previous[name] !== next[name]) return true;
117
- return false;
118
- }
119
- #subscribeToQuery() {
120
- const options = () => this.#options;
121
- return this.#query.subscribe(() => this.updateResult(), {
122
- get refetchOnReconnect() {
123
- return options().refetchOnReconnect;
124
- },
125
- get refetchOnWindowFocus() {
126
- return options().refetchOnWindowFocus;
158
+ }
159
+ /**
160
+ * The current result, then every change listeners are notified of. Counts
161
+ * as a listener while the stream runs, so the query counts as read.
162
+ */
163
+ function changes(self) {
164
+ return listen((push) => {
165
+ let last;
166
+ const emit = () => {
167
+ const result = getUnsafe(self);
168
+ if (result !== last) {
169
+ last = result;
170
+ push(result);
127
171
  }
128
- });
129
- }
130
- #startInterval() {
131
- clearInterval(this.#interval);
132
- this.#interval = void 0;
133
- const interval = this.#options.refetchInterval;
134
- if (interval === void 0 || !Number.isFinite(interval) || interval <= 0) return;
135
- this.#interval = setInterval(() => {
136
- if (!("document" in globalThis) || document.visibilityState !== "hidden") this.#query.refetch();
137
- }, interval);
138
- }
139
- #createResult(query, options) {
140
- const state = query.getState();
141
- const last = this.#lastWithData;
142
- const previous = options.whilePending === "previous" && !state.hasData && state.status !== "error" && last !== void 0 && last.hash !== query.hash ? last : void 0;
143
- const shown = previous ?? query;
144
- const shownState = previous ? previous.getState() : state;
145
- return {
146
- error: state.error,
147
- fetchNextPage: query.fetchNextPage,
148
- fetchPreviousPage: query.fetchPreviousPage,
149
- hash: query.hash,
150
- hasNextPage: query.hasNextPage(),
151
- hasPreviousPage: query.hasPreviousPage(),
152
- input: shown.handle.input,
153
- isFetching: state.isFetching,
154
- isFetchingNextPage: state.fetchDirection === "forward",
155
- isFetchingPreviousPage: state.fetchDirection === "backward",
156
- isPending: !state.hasData,
157
- isPrevious: previous !== void 0,
158
- isStale: state.isStale,
159
- key: shown.handle.key,
160
- promise: shownState.promise,
161
- refetch: query.refetch,
162
- updatedAt: shownState.updatedAt
163
172
  };
164
- }
165
- };
173
+ const unsubscribe = subscribeUnsafe(self, emit);
174
+ emit();
175
+ return unsubscribe;
176
+ });
177
+ }
178
+ /**
179
+ * Wrap `result` so that reading a property marks it tracked. Once any
180
+ * property is tracked, listeners are only notified when a tracked one
181
+ * changes.
182
+ */
183
+ function trackResult(self, result) {
184
+ return new Proxy(result, { get: (target, name, receiver) => {
185
+ if (isResultKey(name)) trackProp(self, name);
186
+ return Reflect.get(target, name, receiver);
187
+ } });
188
+ }
189
+ /** Mark `name` as read, for bindings that track access themselves. */
190
+ function trackProp(self, name) {
191
+ implOf(self).trackedProps.add(name);
192
+ }
193
+ /** Recompute the result, and notify listeners if a tracked property changed. */
194
+ function updateResult(self) {
195
+ const previous = self.current;
196
+ const next = resultOf(self.query, self.options, self.lastWithData);
197
+ if (getUnsafe$1(self.query).hasData) self.lastWithData = self.query;
198
+ if (shallowEqual(previous, next)) return;
199
+ self.current = next;
200
+ if (shouldNotify(self, previous, next)) for (const listener of self.listeners) listener();
201
+ }
202
+ function shouldNotify(self, previous, next) {
203
+ if (self.trackedProps.size === 0) return true;
204
+ for (const name of self.trackedProps) if (previous[name] !== next[name]) return true;
205
+ return false;
206
+ }
207
+ /**
208
+ * Subscribe to the current query in a new scope under its entry's, and
209
+ * start polling. Replaces any previous subscription.
210
+ */
211
+ function connect(self) {
212
+ disconnect(self);
213
+ const query = self.query;
214
+ const preferences = {
215
+ get refetchOnReconnect() {
216
+ return self.options.refetchOnReconnect;
217
+ },
218
+ get refetchOnWindowFocus() {
219
+ return self.options.refetchOnWindowFocus;
220
+ }
221
+ };
222
+ const scope = Scope.forkUnsafe(entryOf(query).scope);
223
+ self.subscription = {
224
+ polls: Effect.runSync(FiberHandle.make().pipe(Effect.tap(Effect.acquireRelease(Effect.sync(() => subscribeUnsafe$1(query, () => updateResult(self), preferences)), (unsubscribe) => Effect.sync(unsubscribe))), Scope.provide(scope))),
225
+ scope
226
+ };
227
+ restartPolling(self);
228
+ }
229
+ function disconnect(self) {
230
+ const subscription = self.subscription;
231
+ self.subscription = void 0;
232
+ if (subscription) Effect.runFork(Scope.close(subscription.scope, Exit.void));
233
+ }
234
+ /** Run `poll` for the current `refetchInterval`, or stop polling without one. */
235
+ function restartPolling(self) {
236
+ const subscription = self.subscription;
237
+ if (!subscription) return;
238
+ const interval = self.options.refetchInterval;
239
+ const polls = subscription.polls;
240
+ Effect.runForkWith(entryOf(self.query).cache.services)(interval !== void 0 && Number.isFinite(interval) && interval > 0 ? FiberHandle.run(polls, poll(self.query, interval)) : FiberHandle.clear(polls));
241
+ }
242
+ /** Refetch `query` every `interval` ms while the page is visible. */
243
+ function poll(query, interval) {
244
+ return Effect.gen(function* () {
245
+ if ((yield* Platform).isVisibleUnsafe()) yield* Effect.forkChild(refetch(query), { startImmediately: true });
246
+ }).pipe(Effect.delay(interval), Effect.forever);
247
+ }
166
248
  //#endregion
167
- export { QueryObserver };
249
+ export { TypeId, changes, get, getOptimisticResultUnsafe, getUnsafe, isQueryObserver, make, makeUnsafe, observer_exports, setOptionsUnsafe, subscribeUnsafe, trackProp, trackResult };
@@ -1,8 +1,12 @@
1
1
  import { QueryKey } from "./key.mjs";
2
2
  import { Effect, Schedule } from "effect";
3
3
  //#region src/options.d.ts
4
- /** What a `queryFn` may return: a value, a promise, or an `Effect`. */
5
- export type QueryFnResult<TData> = TData | PromiseLike<TData> | Effect.Effect<TData, unknown>;
4
+ /**
5
+ * What a `queryFn` may return: a value, a promise, or an `Effect`. An
6
+ * Effect's error type becomes the query's (promises reject with `unknown`),
7
+ * and the services it needs become requirements of the verbs that start it.
8
+ */
9
+ export type QueryFnResult<TData, TError = unknown, TServices = never> = TData | PromiseLike<TData> | Effect.Effect<TData, TError, TServices>;
6
10
  export interface QueryFunctionContext {
7
11
  /** The query's key, as returned by the factory. */
8
12
  readonly queryKey: QueryKey;
@@ -20,7 +24,7 @@ export interface PageFunctionContext<TPageParam> extends QueryFunctionContext {
20
24
  * of failures so far (1 after the first).
21
25
  */
22
26
  export type RetryPolicy = boolean | number | Schedule.Schedule<unknown, unknown> | ((failureCount: number, error: unknown) => boolean);
23
- /** Options every query accepts. `QueryClient` defaults fill in the rest. */
27
+ /** Options every query accepts. `QueryCache` defaults fill in the rest. */
24
28
  export interface QueryBehavior {
25
29
  /** How long an unread entry stays cached, in ms. `Infinity` keeps it. */
26
30
  readonly gcTime?: number;
@@ -41,12 +45,21 @@ export interface QueryBehavior {
41
45
  */
42
46
  readonly structuralSharing?: boolean;
43
47
  }
44
- export interface QueryOptions<TData> extends QueryBehavior {
48
+ declare const servicesType: unique symbol;
49
+ interface ServicesField<TServices> {
50
+ /**
51
+ * Type-only: the services the `queryFn`'s Effect needs. Never present at
52
+ * runtime. A `queryFn`'s return type can't carry them on its own: with
53
+ * `unknown` data, any return type is assignable to `QueryFnResult`.
54
+ */
55
+ readonly [servicesType]?: () => TServices;
56
+ }
57
+ export interface QueryOptions<TData, TError = unknown, TServices = never> extends QueryBehavior, ServicesField<TServices> {
45
58
  readonly kind: "query";
46
- queryFn(context: QueryFunctionContext): QueryFnResult<TData>;
59
+ queryFn(context: QueryFunctionContext): QueryFnResult<TData, TError, TServices>;
47
60
  readonly queryKey: QueryKey;
48
61
  }
49
- export interface InfiniteQueryOptions<TPage, TPageParam> extends QueryBehavior {
62
+ export interface InfiniteQueryOptions<TPage, TPageParam, TError = unknown, TServices = never> extends QueryBehavior, ServicesField<TServices> {
50
63
  /** Param for the page after `lastPage`, or `null` / `undefined` at the end. */
51
64
  getNextPageParam(lastPage: TPage, pages: ReadonlyArray<TPage>, lastPageParam: TPageParam, pageParams: ReadonlyArray<TPageParam>): TPageParam | null | undefined;
52
65
  /** Param for the page before `firstPage`, or `null` / `undefined` at the start. */
@@ -55,7 +68,7 @@ export interface InfiniteQueryOptions<TPage, TPageParam> extends QueryBehavior {
55
68
  readonly kind: "infinite";
56
69
  /** Keep at most this many pages; the far end is dropped when paging. */
57
70
  readonly maxPages?: number;
58
- queryFn(context: PageFunctionContext<TPageParam>): QueryFnResult<TPage>;
71
+ queryFn(context: PageFunctionContext<TPageParam>): QueryFnResult<TPage, TError, TServices>;
59
72
  readonly queryKey: QueryKey;
60
73
  }
61
74
  /** Data of a paged query. */
@@ -63,7 +76,7 @@ export interface InfiniteData<TPage, TPageParam = unknown> {
63
76
  readonly pageParams: ReadonlyArray<TPageParam>;
64
77
  readonly pages: ReadonlyArray<TPage>;
65
78
  }
66
- export type AnyQueryOptions = QueryOptions<unknown> | InfiniteQueryOptions<unknown, unknown>;
79
+ export type AnyQueryOptions = QueryOptions<unknown, unknown, unknown> | InfiniteQueryOptions<unknown, unknown, unknown, unknown>;
67
80
  /**
68
81
  * Typed options for a query factory:
69
82
  *
@@ -75,16 +88,28 @@ export type AnyQueryOptions = QueryOptions<unknown> | InfiniteQueryOptions<unkno
75
88
  * });
76
89
  * }
77
90
  * ```
91
+ *
92
+ * A `queryFn` Effect may need services; they're inferred, and every verb
93
+ * that can start the query requires them.
78
94
  */
79
- export declare function queryOptions<TData>(options: Omit<QueryOptions<TData>, "kind">): QueryOptions<TData>;
95
+ export declare function queryOptions<TData, TError = unknown, TServices = never>(options: Omit<QueryOptions<TData, TError, TServices>, "kind">): QueryOptions<TData, TError, TServices>;
80
96
  /** Typed options for a paged query factory. */
81
- export declare function infiniteQueryOptions<TPage, TPageParam>(options: Omit<InfiniteQueryOptions<TPage, TPageParam>, "kind">): InfiniteQueryOptions<TPage, TPageParam>;
97
+ export declare function infiniteQueryOptions<TPage, TPageParam, TError = unknown, TServices = never>(options: Omit<InfiniteQueryOptions<TPage, TPageParam, TError, TServices>, "kind">): InfiniteQueryOptions<TPage, TPageParam, TError, TServices>;
98
+ interface FactoryResult {
99
+ readonly kind: "infinite" | "query";
100
+ readonly queryKey: QueryKey;
101
+ }
82
102
  /**
83
103
  * A function of zero or one argument returning `queryOptions(...)` or
84
- * `infiniteQueryOptions(...)`. A second parameter is a type error: the single
104
+ * `infiniteQueryOptions(...)` whose `queryFn` needs at most `TServices`
105
+ * (any services by default). A second parameter is a type error: the single
85
106
  * argument is the query's input, and a handle carries it.
107
+ *
108
+ * The return type names no `queryFn`, and no services for the default:
109
+ * TypeScript infers an inline factory's `queryOptions` type arguments from
110
+ * it, and an `unknown` there would infer `unknown` services.
86
111
  */
87
- export type QueryFactory = (input: never) => AnyQueryOptions;
112
+ export type QueryFactory<TServices = unknown> = (input: never) => unknown extends TServices ? FactoryResult : FactoryResult & ServicesField<TServices>;
88
113
  /** The factory's input type; `undefined` for factories without one. */
89
114
  export type InputOf<TFactory extends QueryFactory> = Parameters<TFactory> extends [] ? undefined : Parameters<TFactory>[0];
90
115
  /** Arguments after the factory: the input, optional when the factory's is. */
@@ -94,9 +119,20 @@ export type IsPaged<TFactory extends QueryFactory> = ReturnType<TFactory> extend
94
119
  readonly kind: "infinite";
95
120
  } ? true : false;
96
121
  /** The data a factory's query resolves to. */
97
- export type DataOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer TPage, infer TPageParam> ? InfiniteData<TPage, TPageParam> : ReturnType<TFactory> extends QueryOptions<infer TData> ? TData : never;
122
+ export type DataOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer TPage, infer TPageParam, infer _TError, infer _TServices> ? InfiniteData<TPage, TPageParam> : ReturnType<TFactory> extends QueryOptions<infer TData, infer _TError, infer _TServices> ? TData : never;
123
+ /**
124
+ * The error a factory's request fails with: the `queryFn`'s Effect error
125
+ * type, or `unknown` for promises and plain values.
126
+ */
127
+ export type ErrorOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer _TPage, infer _TPageParam, infer TError, infer _TServices> ? TError : ReturnType<TFactory> extends QueryOptions<infer _TData, infer TError, infer _TServices> ? TError : unknown;
128
+ /**
129
+ * The services a factory's `queryFn` Effect needs: what `ensure`, `fetch`,
130
+ * `prefetch`, `getQuery` and `watch` require. `never` for promises and plain
131
+ * values, and for a factory of no particular query.
132
+ */
133
+ export type ServicesOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer _TPage, infer _TPageParam, infer _TError, infer TServices> ? TServices : ReturnType<TFactory> extends QueryOptions<infer _TData, infer _TError, infer TServices> ? TServices : never;
98
134
  /** Page type of a paged factory. */
99
- export type PageOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer TPage, unknown> ? TPage : never;
135
+ export type PageOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer TPage, unknown, infer _TError, infer _TServices> ? TPage : never;
100
136
  /** `ensure` / `fetch` options. Only paged factories accept `pages`. */
101
137
  export type LoadOptions<TFactory extends QueryFactory> = IsPaged<TFactory> extends true ? {
102
138
  /** Load this many pages up front (default 1). */
package/dist/options.mjs CHANGED
@@ -10,6 +10,9 @@
10
10
  * });
11
11
  * }
12
12
  * ```
13
+ *
14
+ * A `queryFn` Effect may need services; they're inferred, and every verb
15
+ * that can start the query requires them.
13
16
  */
14
17
  function queryOptions(options) {
15
18
  return {
@@ -0,0 +1,27 @@
1
+ import { Context, Effect } from "effect";
2
+ declare namespace platform_d_exports {
3
+ export { Platform, PlatformEvent, whenOnline };
4
+ }
5
+ /** `"focus"`: the page became visible. `"online"`: the browser reconnected. */
6
+ export type PlatformEvent = "focus" | "online";
7
+ /**
8
+ * The page a cache runs in: whether there is one, and its focus and network
9
+ * state. The default reads the global `window`, `document` and `navigator`;
10
+ * provide another to run a cache against a fake page.
11
+ */
12
+ export interface Platform {
13
+ isOnlineUnsafe(): boolean;
14
+ /** No page to watch: a server render. */
15
+ readonly isServer: boolean;
16
+ isVisibleUnsafe(): boolean;
17
+ /** Call `listener` on every focus and reconnect until unsubscribed. */
18
+ subscribeUnsafe(listener: (event: PlatformEvent) => void): () => void;
19
+ }
20
+ export declare const Platform: Context.Reference<Platform>;
21
+ /**
22
+ * Succeeds now if the platform is online, else on its next `"online"` event.
23
+ * Interrupting it unsubscribes.
24
+ */
25
+ export declare const whenOnline: Effect.Effect<void>;
26
+ //#endregion
27
+ export { platform_d_exports };
@@ -0,0 +1,44 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import { Context, Effect } from "effect";
3
+ //#region src/platform.ts
4
+ var platform_exports = /* @__PURE__ */ __exportAll({
5
+ Platform: () => Platform,
6
+ whenOnline: () => whenOnline
7
+ });
8
+ const globalPlatform = {
9
+ isOnlineUnsafe: () => !("window" in globalThis) || !("navigator" in globalThis) || !("onLine" in navigator) || navigator.onLine,
10
+ get isServer() {
11
+ return !("window" in globalThis);
12
+ },
13
+ isVisibleUnsafe: () => !("document" in globalThis) || document.visibilityState !== "hidden",
14
+ subscribeUnsafe(listener) {
15
+ if (!("window" in globalThis)) return () => {};
16
+ const onVisibilityChange = () => {
17
+ if (document.visibilityState === "visible") listener("focus");
18
+ };
19
+ const onOnline = () => listener("online");
20
+ window.addEventListener("visibilitychange", onVisibilityChange);
21
+ window.addEventListener("online", onOnline);
22
+ return () => {
23
+ window.removeEventListener("visibilitychange", onVisibilityChange);
24
+ window.removeEventListener("online", onOnline);
25
+ };
26
+ }
27
+ };
28
+ const Platform = Context.Reference("@raf-query/core/Platform", { defaultValue: () => globalPlatform });
29
+ /**
30
+ * Succeeds now if the platform is online, else on its next `"online"` event.
31
+ * Interrupting it unsubscribes.
32
+ */
33
+ const whenOnline = Effect.gen(function* () {
34
+ const platform = yield* Platform;
35
+ if (platform.isOnlineUnsafe()) return;
36
+ yield* Effect.callback((resume) => {
37
+ const unsubscribe = platform.subscribeUnsafe((event) => {
38
+ if (event === "online") resume(Effect.void);
39
+ });
40
+ return Effect.sync(unsubscribe);
41
+ });
42
+ });
43
+ //#endregion
44
+ export { Platform, platform_exports, whenOnline };