fetchium 0.2.1 → 0.2.3
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/CHANGELOG.md +14 -0
- package/dist/cjs/development/QueryAdapter-DUo338ga.js.map +1 -1
- package/dist/cjs/development/{QueryClient-vYETX2J2.js → QueryClient-Ce5Mnumb.js} +2 -2
- package/dist/cjs/development/QueryClient-Ce5Mnumb.js.map +1 -0
- package/dist/cjs/development/index.js +1 -1
- package/dist/cjs/{production/mutation-BnIsaYdm.js → development/mutation-GI_gTQEB.js} +2 -2
- package/dist/cjs/development/mutation-GI_gTQEB.js.map +1 -0
- package/dist/cjs/development/react/index.js +1 -1
- package/dist/cjs/development/rest/index.js +1 -1
- package/dist/cjs/development/topic/index.js +1 -1
- package/dist/cjs/development/topic/index.js.map +1 -1
- package/dist/cjs/production/QueryAdapter-DUo338ga.js.map +1 -1
- package/dist/cjs/production/{QueryClient-DJoA1ac6.js → QueryClient-BXGk-5PR.js} +2 -2
- package/dist/cjs/production/QueryClient-BXGk-5PR.js.map +1 -0
- package/dist/cjs/production/index.js +1 -1
- package/dist/cjs/{development/mutation-Beh3eks8.js → production/mutation-Bleah98u.js} +2 -2
- package/dist/cjs/production/mutation-Bleah98u.js.map +1 -0
- package/dist/cjs/production/react/index.js +1 -1
- package/dist/cjs/production/rest/index.js +1 -1
- package/dist/cjs/production/topic/index.js +1 -1
- package/dist/cjs/production/topic/index.js.map +1 -1
- package/dist/esm/QueryAdapter.d.ts +11 -0
- package/dist/esm/QueryAdapter.d.ts.map +1 -1
- package/dist/esm/QueryClient.d.ts +20 -6
- package/dist/esm/QueryClient.d.ts.map +1 -1
- package/dist/esm/QueryResult.d.ts.map +1 -1
- package/dist/esm/development/QueryAdapter-Bu5UJjE4.js.map +1 -1
- package/dist/esm/development/{QueryClient-zAD_O9xj.js → QueryClient-CmMSNSpt.js} +72 -48
- package/dist/esm/development/QueryClient-CmMSNSpt.js.map +1 -0
- package/dist/esm/development/index.js +2 -2
- package/dist/esm/development/{mutation-lw06SxbJ.js → mutation-BAM3eYqd.js} +2 -2
- package/dist/esm/development/mutation-BAM3eYqd.js.map +1 -0
- package/dist/esm/development/react/index.js +1 -1
- package/dist/esm/development/rest/index.js +2 -2
- package/dist/esm/development/topic/index.js +19 -18
- package/dist/esm/development/topic/index.js.map +1 -1
- package/dist/esm/mutation.d.ts +3 -3
- package/dist/esm/mutation.d.ts.map +1 -1
- package/dist/esm/production/QueryAdapter-Bu5UJjE4.js.map +1 -1
- package/dist/esm/production/{QueryClient-DSAzqTG6.js → QueryClient-3aWu_mJE.js} +62 -44
- package/dist/esm/production/QueryClient-3aWu_mJE.js.map +1 -0
- package/dist/esm/production/index.js +2 -2
- package/dist/esm/production/{mutation-Dmb9k9FG.js → mutation-YpiJLNWU.js} +2 -2
- package/dist/esm/production/mutation-YpiJLNWU.js.map +1 -0
- package/dist/esm/production/react/index.js +1 -1
- package/dist/esm/production/rest/index.js +2 -2
- package/dist/esm/production/topic/index.js +19 -18
- package/dist/esm/production/topic/index.js.map +1 -1
- package/dist/esm/query.d.ts +3 -3
- package/dist/esm/query.d.ts.map +1 -1
- package/dist/esm/topic/TopicQuery.d.ts +3 -2
- package/dist/esm/topic/TopicQuery.d.ts.map +1 -1
- package/package.json +1 -1
- package/plugin/docs/api/fetchium.md +2 -2
- package/plugin/docs/api/stores-async.md +9 -3
- package/plugin/docs/api/stores-sync.md +9 -3
- package/plugin/docs/core/entities.md +2 -2
- package/plugin/docs/core/queries.md +12 -19
- package/plugin/docs/core/streaming.md +7 -9
- package/plugin/docs/data/mutations.md +1 -1
- package/plugin/docs/guides/auth.md +65 -42
- package/plugin/docs/guides/error-handling.md +9 -5
- package/plugin/docs/guides/offline.md +11 -8
- package/plugin/docs/guides/testing.md +1 -1
- package/plugin/docs/quickstart.md +1 -1
- package/plugin/docs/reference/rest-queries.md +9 -9
- package/plugin/docs/setup/project-setup.md +5 -5
- package/dist/cjs/development/QueryClient-vYETX2J2.js.map +0 -1
- package/dist/cjs/development/mutation-Beh3eks8.js.map +0 -1
- package/dist/cjs/production/QueryClient-DJoA1ac6.js.map +0 -1
- package/dist/cjs/production/mutation-BnIsaYdm.js.map +0 -1
- package/dist/esm/development/QueryClient-zAD_O9xj.js.map +0 -1
- package/dist/esm/development/mutation-lw06SxbJ.js.map +0 -1
- package/dist/esm/production/QueryClient-DSAzqTG6.js.map +0 -1
- package/dist/esm/production/mutation-Dmb9k9FG.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as r, E as s, G as t, L as o, a as n, M as i, N as M, b as g, c as u, d as y, e as f, Q as C, f as N, g as Q, h as d, R as k, i as l, j as p, k as m, q as E, r as R, t as w } from "./QueryClient-
|
|
1
|
+
import { A as r, E as s, G as t, L as o, a as n, M as i, N as M, b as g, c as u, d as y, e as f, Q as C, f as N, g as Q, h as d, R as k, i as l, j as p, k as m, q as E, r as R, t as w } from "./QueryClient-3aWu_mJE.js";
|
|
2
2
|
import { Q as F } from "./QueryAdapter-Bu5UJjE4.js";
|
|
3
|
-
import { M as A, g as K, m as Y } from "./mutation-
|
|
3
|
+
import { M as A, g as K, m as Y } from "./mutation-YpiJLNWU.js";
|
|
4
4
|
export {
|
|
5
5
|
r as ARRAY_KEY,
|
|
6
6
|
s as Entity,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getContext as h } from "signalium";
|
|
2
|
-
import { l as y, h as D, m as w, V as u, t as d } from "./QueryClient-
|
|
2
|
+
import { l as y, h as D, m as w, V as u, t as d } from "./QueryClient-3aWu_mJE.js";
|
|
3
3
|
class x {
|
|
4
4
|
static adapter;
|
|
5
5
|
params;
|
|
@@ -55,4 +55,4 @@ export {
|
|
|
55
55
|
S as g,
|
|
56
56
|
C as m
|
|
57
57
|
};
|
|
58
|
-
//# sourceMappingURL=mutation-
|
|
58
|
+
//# sourceMappingURL=mutation-YpiJLNWU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutation-YpiJLNWU.js","sources":["../../../src/mutation.ts"],"sourcesContent":["import { getContext, ReactiveTask } from 'signalium';\nimport { ExtractType, InternalTypeDef, MutationEffects, TypeDef, RetryConfig, TypeDefShape } from './types.js';\nimport { QueryClientContext, type QueryContext } from './QueryClient.js';\nimport { ValidatorDef, t } from './typeDefs.js';\nimport { createDefinitionProxy, extractDefinition, type CapturedDefinition } from './fieldRef.js';\nimport type { QueryAdapter, QueryAdapterClass } from './QueryAdapter.js';\n\n// ================================\n// Mutation Definition Types\n// ================================\n\nexport interface MutationConfigOptions {\n retry?: RetryConfig | number | false;\n}\n\nexport interface MutationDefinition<Request, Response> {\n id: string;\n requestShape: InternalTypeDef;\n responseShape: InternalTypeDef | undefined;\n captured: CapturedDefinition<Mutation>;\n optimisticUpdates: boolean;\n config?: MutationConfigOptions;\n effects?: MutationEffects;\n hasGetEffects: boolean;\n adapterClass: QueryAdapterClass;\n}\n\n// ================================\n// Mutation base class\n// ================================\n\nexport abstract class Mutation {\n static adapter?: QueryAdapterClass;\n\n readonly params?: TypeDefShape;\n readonly result?: TypeDefShape;\n readonly optimisticUpdates?: boolean;\n readonly config?: MutationConfigOptions;\n readonly effects?: Readonly<MutationEffects>;\n\n declare context: QueryContext;\n\n abstract getIdentityKey(): unknown;\n\n getEffects?(): MutationEffects;\n\n constructor() {\n return createDefinitionProxy(this);\n }\n}\n\n// ================================\n// Mutation definition cache and lookup\n// ================================\n\nconst mutationDefCache = new WeakMap<new () => Mutation, () => MutationDefinition<any, any>>();\n\nexport const mutationKeyForClass = (cls: new () => Mutation): string => {\n const getMutationDef = mutationDefCache.get(cls);\n\n if (getMutationDef === undefined) {\n throw new Error('Mutation definition not found');\n }\n\n return getMutationDef().id;\n};\n\n// ================================\n// Internal: build mutation definition from class\n// ================================\n\nfunction buildMutationDefinition(MutationClass: new () => Mutation): () => MutationDefinition<any, any> {\n let cached = mutationDefCache.get(MutationClass);\n\n if (cached !== undefined) {\n return cached;\n }\n\n let mutationDefinition: MutationDefinition<any, any> | undefined;\n\n const getter = (): MutationDefinition<any, any> => {\n if (mutationDefinition !== undefined) {\n return mutationDefinition;\n }\n\n const instance = new MutationClass();\n const captured = extractDefinition(instance);\n const { fields } = captured;\n\n const id = `mutation:${String(captured.methods.getIdentityKey.call(fields))}`;\n\n const requestDef = fields.params ?? {};\n const requestShape = (requestDef instanceof ValidatorDef\n ? requestDef\n : t.object(requestDef)) as unknown as InternalTypeDef;\n const responseDef = fields.result;\n const responseShape =\n responseDef !== undefined\n ? ((responseDef instanceof ValidatorDef ? responseDef : t.object(responseDef)) as unknown as InternalTypeDef)\n : undefined;\n\n const adapterClass = (MutationClass as typeof Mutation).adapter;\n if (!adapterClass) {\n throw new Error(\n `Mutation class \"${MutationClass.name}\" must define a static \\`adapter\\` property. ` +\n `Extend RESTMutation (from fetchium/rest) or set \\`static adapter = MyAdapter\\` on your mutation class.`,\n );\n }\n\n mutationDefinition = {\n id,\n requestShape,\n responseShape,\n captured,\n optimisticUpdates: fields.optimisticUpdates ?? false,\n config: fields.config,\n effects: fields.effects,\n hasGetEffects: typeof captured.methods.getEffects === 'function',\n adapterClass,\n };\n\n return mutationDefinition;\n };\n\n mutationDefCache.set(MutationClass, getter);\n return getter;\n}\n\n// ================================\n// Public API\n// ================================\n\nexport function getMutation<T extends Mutation>(\n MutationClass: new () => T,\n): ReactiveTask<Readonly<ExtractType<T['result']>>, [ExtractType<T['params']>]> {\n const getMutationDef = buildMutationDefinition(MutationClass);\n\n const queryClient = getContext(QueryClientContext);\n\n if (queryClient === undefined) {\n throw new Error('QueryClient not found');\n }\n\n return queryClient.getMutation<any, any>(getMutationDef());\n}\n"],"names":["Mutation","createDefinitionProxy","mutationDefCache","mutationKeyForClass","cls","getMutationDef","buildMutationDefinition","MutationClass","cached","mutationDefinition","getter","instance","captured","extractDefinition","fields","id","requestDef","requestShape","ValidatorDef","t","responseDef","responseShape","adapterClass","getMutation","queryClient","getContext","QueryClientContext"],"mappings":";;AA+BO,MAAeA,EAAS;AAAA,EAC7B,OAAO;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAQT,cAAc;AACZ,WAAOC,EAAsB,IAAI;AAAA,EACnC;AACF;AAMA,MAAMC,wBAAuB,QAAA,GAEhBC,IAAsB,CAACC,MAAoC;AACtE,QAAMC,IAAiBH,EAAiB,IAAIE,CAAG;AAE/C,MAAIC,MAAmB;AACrB,UAAM,IAAI,MAAM,+BAA+B;AAGjD,SAAOA,IAAiB;AAC1B;AAMA,SAASC,EAAwBC,GAAuE;AACtG,MAAIC,IAASN,EAAiB,IAAIK,CAAa;AAE/C,MAAIC,MAAW;AACb,WAAOA;AAGT,MAAIC;AAEJ,QAAMC,IAAS,MAAoC;AACjD,QAAID,MAAuB;AACzB,aAAOA;AAGT,UAAME,IAAW,IAAIJ,EAAA,GACfK,IAAWC,EAAkBF,CAAQ,GACrC,EAAE,QAAAG,MAAWF,GAEbG,IAAK,YAAY,OAAOH,EAAS,QAAQ,eAAe,KAAKE,CAAM,CAAC,CAAC,IAErEE,IAAaF,EAAO,UAAU,CAAA,GAC9BG,IAAgBD,aAAsBE,IACxCF,IACAG,EAAE,OAAOH,CAAU,GACjBI,IAAcN,EAAO,QACrBO,IACJD,MAAgB,SACVA,aAAuBF,IAAeE,IAAcD,EAAE,OAAOC,CAAW,IAC1E,QAEAE,IAAgBf,EAAkC;AACxD,QAAI,CAACe;AACH,YAAM,IAAI;AAAA,QACR,mBAAmBf,EAAc,IAAI;AAAA,MAAA;AAKzC,WAAAE,IAAqB;AAAA,MACnB,IAAAM;AAAA,MACA,cAAAE;AAAA,MACA,eAAAI;AAAA,MACA,UAAAT;AAAA,MACA,mBAAmBE,EAAO,qBAAqB;AAAA,MAC/C,QAAQA,EAAO;AAAA,MACf,SAASA,EAAO;AAAA,MAChB,eAAe,OAAOF,EAAS,QAAQ,cAAe;AAAA,MACtD,cAAAU;AAAA,IAAA,GAGKb;AAAA,EACT;AAEA,SAAAP,EAAiB,IAAIK,GAAeG,CAAM,GACnCA;AACT;AAMO,SAASa,EACdhB,GAC8E;AAC9E,QAAMF,IAAiBC,EAAwBC,CAAa,GAEtDiB,IAAcC,EAAWC,CAAkB;AAEjD,MAAIF,MAAgB;AAClB,UAAM,IAAI,MAAM,uBAAuB;AAGzC,SAAOA,EAAY,YAAsBnB,GAAgB;AAC3D;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as v, o as U, f as O } from "../QueryClient-
|
|
1
|
+
import { n as v, o as U, f as O } from "../QueryClient-3aWu_mJE.js";
|
|
2
2
|
import { Q as T } from "../QueryAdapter-Bu5UJjE4.js";
|
|
3
|
-
import { M as w } from "../mutation-
|
|
3
|
+
import { M as w } from "../mutation-YpiJLNWU.js";
|
|
4
4
|
class P extends T {
|
|
5
5
|
_fetch;
|
|
6
6
|
_baseUrl;
|
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
import { f as l } from "../QueryClient-
|
|
1
|
+
import { f as l } from "../QueryClient-3aWu_mJE.js";
|
|
2
2
|
import { Q as d } from "../QueryAdapter-Bu5UJjE4.js";
|
|
3
|
-
class
|
|
4
|
-
static adapter;
|
|
5
|
-
getIdentityKey() {
|
|
6
|
-
return `topic:${this.topic}`;
|
|
7
|
-
}
|
|
8
|
-
getConfig() {
|
|
9
|
-
return {
|
|
10
|
-
staleTime: 0,
|
|
11
|
-
subscribe: () => () => {
|
|
12
|
-
this._topicAdapter?.unsubscribe(this.topic);
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
class _ extends d {
|
|
3
|
+
class f extends d {
|
|
18
4
|
_topics = /* @__PURE__ */ new Map();
|
|
19
5
|
/**
|
|
20
6
|
* Resolve the pending promise for a topic with initial data.
|
|
@@ -79,8 +65,23 @@ class _ extends d {
|
|
|
79
65
|
this.queryClient.applyMutationEvent(e);
|
|
80
66
|
}
|
|
81
67
|
}
|
|
68
|
+
class _ extends l {
|
|
69
|
+
// Explicit type lets subclasses override with adapters that take constructor args.
|
|
70
|
+
static adapter = f;
|
|
71
|
+
getIdentityKey() {
|
|
72
|
+
return `topic:${this.topic}`;
|
|
73
|
+
}
|
|
74
|
+
getConfig() {
|
|
75
|
+
return {
|
|
76
|
+
staleTime: 0,
|
|
77
|
+
subscribe: () => () => {
|
|
78
|
+
this._topicAdapter?.unsubscribe(this.topic);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
82
83
|
export {
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
_ as TopicQuery,
|
|
85
|
+
f as TopicQueryAdapter
|
|
85
86
|
};
|
|
86
87
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../src/topic/
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/topic/TopicQueryAdapter.ts","../../../../src/topic/TopicQuery.ts"],"sourcesContent":["import { QueryAdapter } from '../QueryAdapter.js';\nimport type { Query } from '../query.js';\nimport type { MutationEvent } from '../types.js';\n\n// ================================\n// TopicQueryAdapter — abstract adapter for topic-based subscriptions\n// ================================\n\ninterface TopicCtx extends Query {\n topic: string;\n _topicAdapter?: TopicQueryAdapter;\n}\n\ninterface TopicState {\n status: 'pending' | 'fulfilled' | 'rejected';\n promise?: Promise<unknown>;\n resolve?: (data: unknown) => void;\n reject?: (error: unknown) => void;\n data?: unknown;\n error?: unknown;\n}\n\nexport abstract class TopicQueryAdapter extends QueryAdapter {\n private _topics = new Map<string, TopicState>();\n\n /**\n * Called when a query activates for a given topic.\n * Implementations should start delivering data for this topic,\n * calling `fulfillTopic()` when initial data is available and\n * `sendMutationEvent()` for ongoing updates.\n */\n abstract subscribe(topic: string): void;\n\n /**\n * Called when the query deactivates. Implementations should\n * tear down any resources for this topic.\n */\n abstract unsubscribe(topic: string): void;\n\n /**\n * Resolve the pending promise for a topic with initial data.\n * Can be called before `send()` — the data will be picked up\n * when the query activates.\n */\n protected fulfillTopic(topic: string, data: unknown): void {\n const state = this._topics.get(topic);\n\n if (state === undefined) {\n this._topics.set(topic, { status: 'fulfilled', data });\n return;\n }\n\n if (state.status === 'pending') {\n state.status = 'fulfilled';\n state.data = data;\n state.resolve!(data);\n }\n }\n\n /**\n * Reject the pending promise for a topic.\n * Can be called before `send()` — the error will be propagated\n * when the query activates.\n */\n protected rejectTopic(topic: string, error: unknown): void {\n const state = this._topics.get(topic);\n\n if (state === undefined) {\n this._topics.set(topic, { status: 'rejected', error });\n return;\n }\n\n if (state.status === 'pending') {\n state.status = 'rejected';\n state.error = error;\n state.reject!(error);\n }\n }\n\n /**\n * Clears internal state for a topic. Called automatically by\n * `unsubscribe` — subclasses generally don't need to call this.\n */\n protected clearTopic(topic: string): void {\n this._topics.delete(topic);\n }\n\n protected clearAll(): void {\n this._topics.clear();\n }\n\n override async send(ctx: Query, _signal: AbortSignal): Promise<unknown> {\n const topicCtx = ctx as TopicCtx;\n topicCtx._topicAdapter = this;\n const topic = topicCtx.topic;\n\n const existing = this._topics.get(topic);\n\n if (existing) {\n switch (existing.status) {\n case 'fulfilled':\n return existing.data;\n case 'rejected':\n throw existing.error;\n case 'pending':\n return existing.promise;\n }\n }\n\n // No state yet — create a deferred and subscribe\n let resolve!: (data: unknown) => void;\n let reject!: (error: unknown) => void;\n const promise = new Promise<unknown>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n\n this._topics.set(topic, { status: 'pending', promise, resolve, reject });\n this.subscribe(topic);\n\n return promise;\n }\n\n /**\n * Convenience wrapper — pushes a mutation event through the QueryClient\n * so that entities and live collections are updated reactively.\n */\n protected sendMutationEvent(event: MutationEvent): void {\n this.queryClient!.applyMutationEvent(event);\n }\n}\n","import { Query } from '../query.js';\nimport { TopicQueryAdapter } from './TopicQueryAdapter.js';\nimport type { QueryAdapterClass } from '../QueryAdapter.js';\nimport type { QueryConfigOptions } from '../query-types.js';\n\n// ================================\n// TopicQuery — declarative topic-based query definition\n// ================================\n\nexport abstract class TopicQuery extends Query {\n // Explicit type lets subclasses override with adapters that take constructor args.\n static override adapter: QueryAdapterClass<TopicQueryAdapter> = TopicQueryAdapter;\n\n abstract topic: string;\n\n getIdentityKey(): string {\n return `topic:${this.topic}`;\n }\n\n getConfig(): QueryConfigOptions {\n return {\n staleTime: 0,\n subscribe: () => {\n return () => {\n const adapter = (this as Record<string, any>)._topicAdapter as TopicQueryAdapter | undefined;\n adapter?.unsubscribe(this.topic);\n };\n },\n };\n }\n}\n"],"names":["TopicQueryAdapter","QueryAdapter","topic","data","state","error","ctx","_signal","topicCtx","existing","resolve","reject","promise","res","rej","event","TopicQuery","Query"],"mappings":";;AAsBO,MAAeA,UAA0BC,EAAa;AAAA,EACnD,8BAAc,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBZ,aAAaC,GAAeC,GAAqB;AACzD,UAAMC,IAAQ,KAAK,QAAQ,IAAIF,CAAK;AAEpC,QAAIE,MAAU,QAAW;AACvB,WAAK,QAAQ,IAAIF,GAAO,EAAE,QAAQ,aAAa,MAAAC,GAAM;AACrD;AAAA,IACF;AAEA,IAAIC,EAAM,WAAW,cACnBA,EAAM,SAAS,aACfA,EAAM,OAAOD,GACbC,EAAM,QAASD,CAAI;AAAA,EAEvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOU,YAAYD,GAAeG,GAAsB;AACzD,UAAMD,IAAQ,KAAK,QAAQ,IAAIF,CAAK;AAEpC,QAAIE,MAAU,QAAW;AACvB,WAAK,QAAQ,IAAIF,GAAO,EAAE,QAAQ,YAAY,OAAAG,GAAO;AACrD;AAAA,IACF;AAEA,IAAID,EAAM,WAAW,cACnBA,EAAM,SAAS,YACfA,EAAM,QAAQC,GACdD,EAAM,OAAQC,CAAK;AAAA,EAEvB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMU,WAAWH,GAAqB;AACxC,SAAK,QAAQ,OAAOA,CAAK;AAAA,EAC3B;AAAA,EAEU,WAAiB;AACzB,SAAK,QAAQ,MAAA;AAAA,EACf;AAAA,EAEA,MAAe,KAAKI,GAAYC,GAAwC;AACtE,UAAMC,IAAWF;AACjB,IAAAE,EAAS,gBAAgB;AACzB,UAAMN,IAAQM,EAAS,OAEjBC,IAAW,KAAK,QAAQ,IAAIP,CAAK;AAEvC,QAAIO;AACF,cAAQA,EAAS,QAAA;AAAA,QACf,KAAK;AACH,iBAAOA,EAAS;AAAA,QAClB,KAAK;AACH,gBAAMA,EAAS;AAAA,QACjB,KAAK;AACH,iBAAOA,EAAS;AAAA,MAAA;AAKtB,QAAIC,GACAC;AACJ,UAAMC,IAAU,IAAI,QAAiB,CAACC,GAAKC,MAAQ;AACjD,MAAAJ,IAAUG,GACVF,IAASG;AAAA,IACX,CAAC;AAED,gBAAK,QAAQ,IAAIZ,GAAO,EAAE,QAAQ,WAAW,SAAAU,GAAS,SAAAF,GAAS,QAAAC,GAAQ,GACvE,KAAK,UAAUT,CAAK,GAEbU;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMU,kBAAkBG,GAA4B;AACtD,SAAK,YAAa,mBAAmBA,CAAK;AAAA,EAC5C;AACF;ACzHO,MAAeC,UAAmBC,EAAM;AAAA;AAAA,EAE7C,OAAgB,UAAgDjB;AAAA,EAIhE,iBAAyB;AACvB,WAAO,SAAS,KAAK,KAAK;AAAA,EAC5B;AAAA,EAEA,YAAgC;AAC9B,WAAO;AAAA,MACL,WAAW;AAAA,MACX,WAAW,MACF,MAAM;AAEX,QADiB,KAA6B,eACrC,YAAY,KAAK,KAAK;AAAA,MACjC;AAAA,IACF;AAAA,EAEJ;AACF;"}
|
package/dist/esm/query.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { QueryCacheOptions, QueryConfigOptions, FetchNextConfig, QueryParams } f
|
|
|
3
3
|
import { ValidatorDef } from './typeDefs.js';
|
|
4
4
|
import { HasRequiredKeys, Optionalize, Signalize } from './type-utils.js';
|
|
5
5
|
import { type CapturedDefinition } from './fieldRef.js';
|
|
6
|
-
import type {
|
|
6
|
+
import type { QueryAdapterClass } from './QueryAdapter.js';
|
|
7
7
|
export interface ResolvedRetryConfig {
|
|
8
8
|
retries: number;
|
|
9
9
|
retryDelay: (attempt: number) => number;
|
|
@@ -15,7 +15,7 @@ export declare abstract class Query {
|
|
|
15
15
|
* The adapter class responsible for sending requests for this query type.
|
|
16
16
|
* Must be set on each concrete Query subclass (or inherited from a base like RESTQuery).
|
|
17
17
|
*/
|
|
18
|
-
static adapter?:
|
|
18
|
+
static adapter?: QueryAdapterClass;
|
|
19
19
|
params?: Record<string, TypeDef>;
|
|
20
20
|
abstract result: TypeDefShape;
|
|
21
21
|
config?: QueryConfigOptions;
|
|
@@ -45,7 +45,7 @@ export interface QueryDefinitionStatics {
|
|
|
45
45
|
/** Whether the result shape is already an entity (vs synthetic wrapper). */
|
|
46
46
|
readonly isEntityResult: boolean;
|
|
47
47
|
/** The adapter class responsible for sending requests. */
|
|
48
|
-
readonly adapterClass:
|
|
48
|
+
readonly adapterClass: QueryAdapterClass;
|
|
49
49
|
}
|
|
50
50
|
export declare class QueryDefinition<Params extends QueryParams | undefined, Result, StreamType> {
|
|
51
51
|
readonly captured: CapturedDefinition<Query>;
|
package/dist/esm/query.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/query.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAkB,MAAM,YAAY,CAAC;AAC3G,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EAEf,WAAW,EAEZ,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAK,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/query.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Query } from '../query.js';
|
|
2
|
-
import
|
|
2
|
+
import { TopicQueryAdapter } from './TopicQueryAdapter.js';
|
|
3
|
+
import type { QueryAdapterClass } from '../QueryAdapter.js';
|
|
3
4
|
import type { QueryConfigOptions } from '../query-types.js';
|
|
4
5
|
export declare abstract class TopicQuery extends Query {
|
|
5
|
-
static adapter:
|
|
6
|
+
static adapter: QueryAdapterClass<TopicQueryAdapter>;
|
|
6
7
|
abstract topic: string;
|
|
7
8
|
getIdentityKey(): string;
|
|
8
9
|
getConfig(): QueryConfigOptions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TopicQuery.d.ts","sourceRoot":"","sources":["../../../src/topic/TopicQuery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"TopicQuery.d.ts","sourceRoot":"","sources":["../../../src/topic/TopicQuery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAM5D,8BAAsB,UAAW,SAAQ,KAAK;IAE5C,OAAgB,OAAO,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAqB;IAElF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,cAAc,IAAI,MAAM;IAIxB,SAAS,IAAI,kBAAkB;CAWhC"}
|
package/package.json
CHANGED
|
@@ -226,7 +226,7 @@ new QueryClient(config: QueryClientConfig)
|
|
|
226
226
|
|
|
227
227
|
| Field | Type | Default | Description |
|
|
228
228
|
| -------------------- | ---------------------------- | ---------------------- | ------------------------------------------------------------------------------------ |
|
|
229
|
-
| `store` | `QueryStore` |
|
|
229
|
+
| `store` | `QueryStore` | In-memory | Persistent storage backend. Defaults to `SyncQueryStore(MemoryPersistentStore)`. |
|
|
230
230
|
| `adapters` | `QueryAdapter[]` | `[]` | Transport adapters (e.g. `new RESTQueryAdapter({ fetch, baseUrl })`). |
|
|
231
231
|
| `log` | `LogContext \| undefined` | `console` | Logger with `error`, `warn`, `info`, `debug` methods. |
|
|
232
232
|
| `evictionMultiplier` | `number \| undefined` | `1` | Scales all GC times for testing. Set to `0.001` to make timers fire in milliseconds. |
|
|
@@ -604,7 +604,7 @@ The `t` object provides a declarative type definition DSL for describing query p
|
|
|
604
604
|
| `QueryCacheOptions` | `{ maxCount?: number; cacheTime?: number }` | Persistent storage cache settings. `cacheTime` is in minutes (default: 1440 / 24 hours). `maxCount` is the LRU queue size. |
|
|
605
605
|
| `QueryConfigOptions` | `{ gcTime?, staleTime?, debounce?, networkMode?, retry?, refreshStaleOnReconnect?, subscribe? }` | Instance-level query configuration. See property table below. |
|
|
606
606
|
| `QueryRequestOptions` | `{ baseUrl?, credentials?, mode?, cache?, redirect?, referrer?, referrerPolicy?, integrity?, keepalive?, signal? }` | Extended fetch options for individual queries. |
|
|
607
|
-
| `QueryContext` | `{
|
|
607
|
+
| `QueryContext` | `{ log?, evictionMultiplier? }` | Context object provided to the `QueryClient`. |
|
|
608
608
|
| `QueryParams` | `Record<string, string \| number \| boolean \| undefined \| null \| Signal<...> \| unknown[] \| Record<string, unknown>>` | The shape of query parameters at runtime. |
|
|
609
609
|
| `FetchNextConfig` | `{ url?: unknown; searchParams?: Record<string, unknown> }` | Pagination configuration. Values can be FieldRefs. |
|
|
610
610
|
|
|
@@ -181,6 +181,7 @@ type StoreMessage =
|
|
|
181
181
|
```ts
|
|
182
182
|
import { QueryClient } from 'fetchium';
|
|
183
183
|
import { AsyncQueryStore } from 'fetchium/stores/async';
|
|
184
|
+
import { RESTQueryAdapter } from 'fetchium/rest';
|
|
184
185
|
|
|
185
186
|
const worker = new Worker('./store-worker.js');
|
|
186
187
|
|
|
@@ -194,9 +195,14 @@ const store = new AsyncQueryStore({
|
|
|
194
195
|
},
|
|
195
196
|
});
|
|
196
197
|
|
|
197
|
-
const client = new QueryClient(
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
const client = new QueryClient({
|
|
199
|
+
store,
|
|
200
|
+
adapters: [
|
|
201
|
+
new RESTQueryAdapter({
|
|
202
|
+
fetch: globalThis.fetch,
|
|
203
|
+
baseUrl: 'https://api.example.com',
|
|
204
|
+
}),
|
|
205
|
+
],
|
|
200
206
|
});
|
|
201
207
|
```
|
|
202
208
|
|
|
@@ -121,13 +121,19 @@ Internally, `SyncQueryStore` uses the following key prefixes in the underlying `
|
|
|
121
121
|
```ts
|
|
122
122
|
import { QueryClient } from 'fetchium';
|
|
123
123
|
import { SyncQueryStore, MemoryPersistentStore } from 'fetchium/stores/sync';
|
|
124
|
+
import { RESTQueryAdapter } from 'fetchium/rest';
|
|
124
125
|
|
|
125
126
|
// Create an in-memory store
|
|
126
127
|
const store = new SyncQueryStore(new MemoryPersistentStore());
|
|
127
128
|
|
|
128
129
|
// Create the query client
|
|
129
|
-
const client = new QueryClient(
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
const client = new QueryClient({
|
|
131
|
+
store,
|
|
132
|
+
adapters: [
|
|
133
|
+
new RESTQueryAdapter({
|
|
134
|
+
fetch: globalThis.fetch,
|
|
135
|
+
baseUrl: 'https://api.example.com',
|
|
136
|
+
}),
|
|
137
|
+
],
|
|
132
138
|
});
|
|
133
139
|
```
|
|
@@ -171,7 +171,7 @@ class User extends Entity {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
greet() {
|
|
174
|
-
return `Hello, ${this.
|
|
174
|
+
return `Hello, ${this.fullName}!`;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
isAdult() {
|
|
@@ -185,7 +185,7 @@ Methods work on entity proxies just like regular methods:
|
|
|
185
185
|
```tsx
|
|
186
186
|
const user = result.user;
|
|
187
187
|
user.fullName; // "Alice Smith"
|
|
188
|
-
user.greet(); // "Hello, Alice!"
|
|
188
|
+
user.greet(); // "Hello, Alice Smith!"
|
|
189
189
|
user.isAdult(); // true
|
|
190
190
|
```
|
|
191
191
|
|
|
@@ -163,17 +163,16 @@ interface ReactivePromise<T> {
|
|
|
163
163
|
|
|
164
164
|
```ts
|
|
165
165
|
type QueryResult<Q extends Query> = Q['result'] & {
|
|
166
|
-
__refetch():
|
|
166
|
+
__refetch(): QueryPromise<Q>;
|
|
167
167
|
__fetchNext(): Promise<Q['result']>;
|
|
168
|
+
__hasNext: boolean;
|
|
169
|
+
__isFetchingNext: boolean;
|
|
168
170
|
};
|
|
169
171
|
|
|
170
172
|
declare function useQuery<Q extends Query>(
|
|
171
|
-
|
|
172
|
-
params
|
|
173
|
-
|
|
174
|
-
suspended?: boolean;
|
|
175
|
-
},
|
|
176
|
-
): ReactivePromise<QueryResult<Q>>;
|
|
173
|
+
QueryClass: new () => Q,
|
|
174
|
+
params?: ExtractQueryParams<Q>,
|
|
175
|
+
): QueryPromise<Q>;
|
|
177
176
|
```
|
|
178
177
|
|
|
179
178
|
The reason `__refetch` and `__fetchNext` are defined on the _result_ of the query and not the `ReactivePromise` is about composability, which leads us into usage within Signalium.
|
|
@@ -190,27 +189,21 @@ import { GetCurrentUser, GetUserProfile } from './queries';
|
|
|
190
189
|
|
|
191
190
|
export function UserProfile() {
|
|
192
191
|
const userResult = useQuery(GetCurrentUser);
|
|
193
|
-
const userProfileResult = useQuery(
|
|
194
|
-
|
|
195
|
-
{ user },
|
|
196
|
-
{ suspended: !user },
|
|
192
|
+
const userProfileResult = useQuery(GetUserProfile, {
|
|
193
|
+
user: userResult.value,
|
|
197
194
|
});
|
|
198
195
|
|
|
199
196
|
if (userResult.isRejected || userProfileResult.isRejected) {
|
|
200
197
|
const message =
|
|
201
|
-
userResult.error?.message ||
|
|
202
|
-
userProfileResult.error?.message;
|
|
198
|
+
userResult.error?.message || userProfileResult.error?.message;
|
|
203
199
|
|
|
204
|
-
return <div>
|
|
205
|
-
Error: {message}
|
|
206
|
-
</div>;
|
|
200
|
+
return <div>Error: {message}</div>;
|
|
207
201
|
}
|
|
208
202
|
|
|
209
203
|
if (!userResult.isReady || !userProfileResult.isReady) {
|
|
210
|
-
|
|
204
|
+
return <div>Loading...</div>;
|
|
211
205
|
}
|
|
212
206
|
|
|
213
|
-
|
|
214
207
|
return (
|
|
215
208
|
<div>
|
|
216
209
|
<h1>{userProfileResult.value.name}</h1>
|
|
@@ -344,7 +337,7 @@ class GetUser extends RESTQuery {
|
|
|
344
337
|
}
|
|
345
338
|
```
|
|
346
339
|
|
|
347
|
-
By convention,
|
|
340
|
+
By convention, most fields provided by `RESTQuery` and other query implementations have a corresponding `get*` method. So for `path` there is `getPath`, for `searchParams` there is `getSearchParams`, for `body` there is `getBody`, and so on.
|
|
348
341
|
|
|
349
342
|
{% callout title="API design by TypeScript limitations" type="note" %}
|
|
350
343
|
The original API design for this feature allowed getters in place of fields, so `get path() {}` would work as well. The issue was that TypeScript does not allow this specific combination on abstract classes at the moment. See [this issue](https://github.com/microsoft/TypeScript/issues/40635) for more information.
|
|
@@ -139,7 +139,7 @@ A topic query extends `TopicQuery` and provides a `topic` field and a `result` s
|
|
|
139
139
|
import { t } from 'fetchium';
|
|
140
140
|
import { TopicQuery } from 'fetchium/topic';
|
|
141
141
|
|
|
142
|
-
class GetPrices extends
|
|
142
|
+
class GetPrices extends TopicQuery {
|
|
143
143
|
topic = 'prices:live';
|
|
144
144
|
|
|
145
145
|
result = {
|
|
@@ -151,7 +151,7 @@ class GetPrices extends MyTopicQuery {
|
|
|
151
151
|
Topics can be parameterized using `this.params`, just like paths in `RESTQuery`:
|
|
152
152
|
|
|
153
153
|
```tsx
|
|
154
|
-
class GetBalances extends
|
|
154
|
+
class GetBalances extends TopicQuery {
|
|
155
155
|
params = { walletId: t.string };
|
|
156
156
|
|
|
157
157
|
topic = `balances:${this.params.walletId}`;
|
|
@@ -231,13 +231,11 @@ const queryClient = new QueryClient({
|
|
|
231
231
|
});
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
Topic query classes that extend `TopicQuery` directly resolve to the registered `MyStreamAdapter` automatically. Internally, `TopicQuery` declares `static adapter = TopicQueryAdapter` (the abstract base), and `QueryClient` looks up registered adapters by `instanceof` match, so any subclass of `TopicQueryAdapter` you register fulfills the lookup.
|
|
235
235
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
```
|
|
236
|
+
{% callout title="One streaming adapter per QueryClient" %}
|
|
237
|
+
Register at most one `TopicQueryAdapter` subclass on a given `QueryClient`. If your app needs multiple streaming protocols, create a separate `QueryClient` for each. Dev builds throw if more than one registered adapter satisfies the same lookup.
|
|
238
|
+
{% /callout %}
|
|
241
239
|
|
|
242
240
|
### Pre-fulfillment
|
|
243
241
|
|
|
@@ -515,7 +513,7 @@ In practice, most applications combine multiple real-time strategies:
|
|
|
515
513
|
|
|
516
514
|
```tsx
|
|
517
515
|
// Topic-based streaming for live market data
|
|
518
|
-
class GetPrices extends
|
|
516
|
+
class GetPrices extends TopicQuery {
|
|
519
517
|
topic = 'prices:live';
|
|
520
518
|
result = { prices: t.liveArray(Price) };
|
|
521
519
|
}
|
|
@@ -364,7 +364,7 @@ import { Mutation, t } from 'fetchium';
|
|
|
364
364
|
class UploadAvatar extends Mutation {
|
|
365
365
|
static override adapter = MyAdapter;
|
|
366
366
|
|
|
367
|
-
params = { userId: t.id
|
|
367
|
+
params = { userId: t.id };
|
|
368
368
|
result = { url: t.string };
|
|
369
369
|
|
|
370
370
|
getIdentityKey() {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
title: Auth & Headers
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
In most data-fetching libraries, authentication is handled through interceptors, middleware chains, or framework-specific hooks. Fetchium takes a different approach: authentication is handled through the `fetch` function you pass to the `
|
|
5
|
+
In most data-fetching libraries, authentication is handled through interceptors, middleware chains, or framework-specific hooks. Fetchium takes a different approach: authentication is handled through the `fetch` function you pass to the `RESTQueryAdapter`.
|
|
6
6
|
|
|
7
7
|
This is intentional. Rather than adding a framework-specific interceptor system, Fetchium leverages the web platform's standard `fetch` API. Your auth logic is a _plain JavaScript function_ --- testable, portable, and completely decoupled from the library. You can unit test it without importing Fetchium, reuse it across projects, or swap it out without touching a single query definition.
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ This page covers the common patterns for adding authentication and custom header
|
|
|
12
12
|
|
|
13
13
|
## Global Headers via a Fetch Wrapper
|
|
14
14
|
|
|
15
|
-
The simplest and most common pattern is wrapping the native `fetch` with a function that injects your auth token on every request. You pass this wrapper to the `
|
|
15
|
+
The simplest and most common pattern is wrapping the native `fetch` with a function that injects your auth token on every request. You pass this wrapper to the `RESTQueryAdapter` at setup time, and every query uses it automatically.
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
18
|
function createAuthFetch(getToken: () => string | null) {
|
|
@@ -28,9 +28,13 @@ function createAuthFetch(getToken: () => string | null) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const client = new QueryClient(
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const client = new QueryClient({
|
|
32
|
+
adapters: [
|
|
33
|
+
new RESTQueryAdapter({
|
|
34
|
+
fetch: createAuthFetch(() => localStorage.getItem('auth_token')),
|
|
35
|
+
baseUrl: 'https://api.example.com',
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
34
38
|
});
|
|
35
39
|
```
|
|
36
40
|
|
|
@@ -45,13 +49,17 @@ Notice that `createAuthFetch` accepts a _getter function_ rather than the token
|
|
|
45
49
|
If your API uses a static key rather than a user token, the pattern is even simpler:
|
|
46
50
|
|
|
47
51
|
```ts
|
|
48
|
-
const client = new QueryClient(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const client = new QueryClient({
|
|
53
|
+
adapters: [
|
|
54
|
+
new RESTQueryAdapter({
|
|
55
|
+
fetch: async (url, init) => {
|
|
56
|
+
const headers = new Headers(init?.headers);
|
|
57
|
+
headers.set('X-API-Key', process.env.API_KEY!);
|
|
58
|
+
return fetch(url, { ...init, headers });
|
|
59
|
+
},
|
|
60
|
+
baseUrl: 'https://api.example.com',
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
55
63
|
});
|
|
56
64
|
```
|
|
57
65
|
|
|
@@ -93,9 +101,13 @@ function createReactiveAuthFetch() {
|
|
|
93
101
|
};
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
const client = new QueryClient(
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
const client = new QueryClient({
|
|
105
|
+
adapters: [
|
|
106
|
+
new RESTQueryAdapter({
|
|
107
|
+
fetch: createReactiveAuthFetch(),
|
|
108
|
+
baseUrl: 'https://api.example.com',
|
|
109
|
+
}),
|
|
110
|
+
],
|
|
99
111
|
});
|
|
100
112
|
```
|
|
101
113
|
|
|
@@ -132,7 +144,7 @@ class UploadAvatar extends RESTQuery {
|
|
|
132
144
|
|
|
133
145
|
The layering is straightforward: your global fetch wrapper handles _auth_ (the concern that applies everywhere), and per-query headers handle _API-specific needs_ (the concerns that vary by endpoint). The two are composed naturally --- `headers` from the query class are passed through `init.headers` to your fetch wrapper, which can merge them with auth headers using `new Headers(init?.headers)`.
|
|
134
146
|
|
|
135
|
-
For dynamic per-query headers that depend on runtime conditions, use
|
|
147
|
+
For dynamic per-query headers that depend on runtime conditions, use `getRequestOptions()` to include headers as part of the fetch options:
|
|
136
148
|
|
|
137
149
|
```ts
|
|
138
150
|
class GetReport extends RESTQuery {
|
|
@@ -143,14 +155,11 @@ class GetReport extends RESTQuery {
|
|
|
143
155
|
|
|
144
156
|
path = `/reports/${this.params.reportId}`;
|
|
145
157
|
|
|
146
|
-
|
|
147
|
-
const headers: Record<string, string> = {};
|
|
148
|
-
|
|
158
|
+
getRequestOptions() {
|
|
149
159
|
if (this.params.format === 'csv') {
|
|
150
|
-
headers
|
|
160
|
+
return { headers: { Accept: 'text/csv' } };
|
|
151
161
|
}
|
|
152
|
-
|
|
153
|
-
return headers;
|
|
162
|
+
return undefined;
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
result = {
|
|
@@ -159,7 +168,7 @@ class GetReport extends RESTQuery {
|
|
|
159
168
|
}
|
|
160
169
|
```
|
|
161
170
|
|
|
162
|
-
As described in the [Queries](/core/queries) guide,
|
|
171
|
+
As described in the [Queries](/core/queries) guide, most fields on `RESTQuery` have a corresponding `get*()` method for when you need logic that goes beyond simple references and interpolations.
|
|
163
172
|
|
|
164
173
|
---
|
|
165
174
|
|
|
@@ -215,13 +224,17 @@ A few things to note in this pattern:
|
|
|
215
224
|
Wire it into your client the same way:
|
|
216
225
|
|
|
217
226
|
```ts
|
|
218
|
-
const client = new QueryClient(
|
|
219
|
-
|
|
220
|
-
(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
227
|
+
const client = new QueryClient({
|
|
228
|
+
adapters: [
|
|
229
|
+
new RESTQueryAdapter({
|
|
230
|
+
fetch: createAuthFetchWithRefresh(
|
|
231
|
+
() => authToken.value,
|
|
232
|
+
() => api.refreshSession(),
|
|
233
|
+
(token) => authToken.set(token),
|
|
234
|
+
),
|
|
235
|
+
baseUrl: 'https://api.example.com',
|
|
236
|
+
}),
|
|
237
|
+
],
|
|
225
238
|
});
|
|
226
239
|
```
|
|
227
240
|
|
|
@@ -236,18 +249,28 @@ If your application talks to multiple APIs with different auth schemes --- for i
|
|
|
236
249
|
The cleanest approach is creating a dedicated `QueryClient` for each backend:
|
|
237
250
|
|
|
238
251
|
```ts
|
|
239
|
-
const appClient = new QueryClient(
|
|
240
|
-
|
|
241
|
-
|
|
252
|
+
const appClient = new QueryClient({
|
|
253
|
+
store: appStore,
|
|
254
|
+
adapters: [
|
|
255
|
+
new RESTQueryAdapter({
|
|
256
|
+
fetch: createAuthFetch(() => authToken.value),
|
|
257
|
+
baseUrl: 'https://api.myapp.com',
|
|
258
|
+
}),
|
|
259
|
+
],
|
|
242
260
|
});
|
|
243
261
|
|
|
244
|
-
const analyticsClient = new QueryClient(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
262
|
+
const analyticsClient = new QueryClient({
|
|
263
|
+
store: analyticsStore,
|
|
264
|
+
adapters: [
|
|
265
|
+
new RESTQueryAdapter({
|
|
266
|
+
fetch: async (url, init) => {
|
|
267
|
+
const headers = new Headers(init?.headers);
|
|
268
|
+
headers.set('X-API-Key', ANALYTICS_API_KEY);
|
|
269
|
+
return fetch(url, { ...init, headers });
|
|
270
|
+
},
|
|
271
|
+
baseUrl: 'https://analytics.example.com',
|
|
272
|
+
}),
|
|
273
|
+
],
|
|
251
274
|
});
|
|
252
275
|
```
|
|
253
276
|
|
|
@@ -294,12 +317,12 @@ This is a deliberate design decision. Fetchium favors _composition over configur
|
|
|
294
317
|
| Global fetch wrapper | Auth that applies to all requests (JWT, session cookies, API keys) |
|
|
295
318
|
| Reactive signal token | SPAs where auth state changes at runtime (login/logout) |
|
|
296
319
|
| Per-query `headers` | Endpoint-specific headers (content types, API versions) |
|
|
297
|
-
| `
|
|
320
|
+
| `getRequestOptions()` method | Dynamic per-query headers based on runtime conditions |
|
|
298
321
|
| 401 catch + refresh + retry | Token expiration with automatic renewal |
|
|
299
322
|
| Multiple `QueryClient` instances | Different APIs with different auth schemes or stores |
|
|
300
323
|
| Per-query `requestOptions.baseUrl` | Same auth, different host |
|
|
301
324
|
|
|
302
|
-
The common thread is that Fetchium does not own your auth logic. It provides the _seam_ --- the `fetch` option on `
|
|
325
|
+
The common thread is that Fetchium does not own your auth logic. It provides the _seam_ --- the `fetch` option on `RESTQueryAdapter` --- and you fill it with whatever your application needs. This keeps the library small, your auth testable, and your options open.
|
|
303
326
|
|
|
304
327
|
---
|
|
305
328
|
|