@ueberbrot/effect-api-query
Version:
Type-safe TanStack Query utilities derived from Effect RPC and HTTP API contracts.
1,818 lines • 72.9 kB
JavaScript
import { CancelledError, experimental_streamedQuery, hashKey, replaceEqualDeep, skipToken, skipToken as skipToken$1 } from "@tanstack/query-core";
import { Array as Array$1, Cause, Duration, Effect, Exit, Function, Option, Order, Predicate, Pull, Result, Schema, SchemaAST, Scope, Stream } from "effect";
import { Rpc, RpcSchema } from "effect/rpc";
import { HttpApi, HttpApiSchema } from "effect/http-api";
//#region src/core/canonical-key.ts
const canonicalizeArray = (value, seen = /* @__PURE__ */ new WeakSet()) => {
if (seen.has(value)) throw new TypeError("Key values must not contain cycles");
seen.add(value);
const copy = [];
for (let index = 0; index < value.length; index += 1) {
if (!Object.hasOwn(value, index)) throw new TypeError("Key values must not contain sparse arrays");
copy.push(canonicalize(value[index], seen));
}
seen.delete(value);
return Object.freeze(copy);
};
const canonicalizeObject = (value, seen) => {
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) throw new TypeError("Key values must contain only plain objects");
const copy = {};
for (const key of Array$1.sort(Object.keys(value), Order.String)) {
if (key === "__proto__" || key === "constructor") throw new TypeError("Key objects must not contain __proto__ or constructor properties");
copy[key] = canonicalize(value[key], seen);
}
seen.delete(value);
return Object.freeze(copy);
};
/** Copies strict JSON into the immutable representation used by keys and filters. */
const canonicalize = (value, seen = /* @__PURE__ */ new WeakSet()) => {
if (value === null || Predicate.isString(value) || Predicate.isBoolean(value)) return value;
if (Predicate.isNumber(value)) {
if (!Number.isFinite(value)) throw new TypeError("Key values must contain only finite numbers");
return Object.is(value, -0) ? 0 : value;
}
if (!Predicate.isObjectOrArray(value)) throw new TypeError("Key values must be JSON-safe");
if (Array.isArray(value)) return canonicalizeArray(value, seen);
if (seen.has(value)) throw new TypeError("Key values must not contain cycles");
seen.add(value);
return canonicalizeObject(value, seen);
};
//#endregion
//#region src/core/metadata-query.ts
const isStructuralSharing = (policy) => Predicate.isFunction(policy);
const shareQueryData = (previous, data, policy) => {
if (policy === false) return data;
if (isStructuralSharing(policy)) return policy(previous, data);
return replaceEqualDeep(previous, data);
};
const fetchedPolicies = /* @__PURE__ */ new WeakMap();
const preserveMetadataPolicy = (previous, data) => {
if (Predicate.isObject(previous) && Predicate.isObject(data)) {
const policy = fetchedPolicies.get(previous);
if (policy !== void 0) fetchedPolicies.set(data, policy);
}
return data;
};
const prepareMetadataExecution = (description, options, runPromiseExit, requestOptions, skipped) => {
const hasLocalPolicy = Object.hasOwn(options, "structuralSharing");
const localPolicy = options["structuralSharing"];
if (!skipped) options["structuralSharing"] = (previous, data) => {
const produced = Predicate.isObject(data) ? fetchedPolicies.get(data) : void 0;
if (Predicate.isObject(data)) fetchedPolicies.delete(data);
const shared = shareQueryData(previous, data, produced === void 0 ? localPolicy : produced.policy);
return produced === void 0 ? shared : description.finalizeData(shared);
};
return (input) => async ({ client, queryKey, signal }) => {
const policy = hasLocalPolicy ? localPolicy : client.defaultQueryOptions({ queryKey }).structuralSharing;
const exit = await runPromiseExit(description.invoke(input, requestOptions), { signal });
if (Exit.isFailure(exit)) throw description.executionError("metadata", exit.cause);
const data = exit.value === void 0 ? null : exit.value;
if (Predicate.isObject(data)) fetchedPolicies.set(data, { policy });
return data;
};
};
//#endregion
//#region src/core/prepared-input.ts
const values = /* @__PURE__ */ new WeakMap();
const owners = /* @__PURE__ */ new WeakMap();
const registerInputOwner = (key, input, encoder) => {
owners.set(key, {
input,
encoder
});
};
const inputKind = (key) => owners.get(key)?.input._tag;
const readPreparedInput = (owner, input) => {
const prepared = Predicate.isObject(input) ? values.get(input) : void 0;
if (prepared !== void 0 && prepared.owner !== owner) throw new TypeError("Prepared input belongs to another utility leaf");
return prepared;
};
/** Constructs and encodes once for repeated keys/options on this utility leaf. */
const prepareInput = (leaf, input) => {
const owner = owners.get(leaf.key);
if (owner === void 0 || owner.input._tag === "Inputless") throw new TypeError("Prepared inputs require a generated input-bearing query leaf");
const prepared = owner.input.prepare(input, owner.encoder);
let keyValue;
try {
keyValue = canonicalize(prepared.keyValue);
} catch (error) {
throw owner.input.invalidKey(error);
}
const token = Object.freeze({ "~effect-api-query/PreparedInput": Object.freeze({}) });
values.set(token, {
owner: owner.input,
input: prepared.input,
keyValue
});
return token;
};
//#endregion
//#region src/core/utility-tree.ts
const reservedPathSegments = /* @__PURE__ */ new Set([
"__proto__",
"constructor",
"infiniteKey",
"infiniteOptions",
"key",
"liveKey",
"liveOptions",
"metadataKey",
"metadataOptions",
"mutationKey",
"mutationOptions",
"prototype",
"queryKey",
"queryOptions",
"streamedKey",
"streamedOptions"
]);
const freezeKey = (parts) => Object.freeze(parts);
const normalizePrefix = (prefix, errors) => {
if (!Array.isArray(prefix) || prefix.length === 0) throw errors.invalidPrefix("Shape");
try {
return canonicalizeArray(prefix);
} catch (error) {
throw errors.invalidPrefix("Value", error);
}
};
const planPaths = (operations, errors) => {
const branchPaths = /* @__PURE__ */ new Map();
const leafPaths = /* @__PURE__ */ new Set();
const plan = [];
for (const operation of operations) {
const segments = operation.path;
if (!Array$1.isReadonlyArrayNonEmpty(segments) || segments.some((segment) => segment.length === 0 || reservedPathSegments.has(segment))) throw errors.invalidPath(operation.id);
const pathKey = JSON.stringify(segments);
if (leafPaths.has(pathKey)) throw errors.pathCollision(operation.id, segments, "duplicates");
const descendantPath = branchPaths.get(pathKey);
if (descendantPath !== void 0) throw errors.pathCollision(operation.id, descendantPath, "collides with");
const parents = [];
for (let index = 1; index < segments.length; index += 1) {
const parent = segments.slice(0, index);
const parentKey = JSON.stringify(parent);
if (leafPaths.has(parentKey)) throw errors.pathCollision(operation.id, parent, "collides with");
parents.push(parentKey);
}
for (const parent of parents) if (!branchPaths.has(parent)) branchPaths.set(parent, segments);
leafPaths.add(pathKey);
plan.push({
operation,
segments
});
}
return plan;
};
const execute = async (description, operation, input, runPromiseExit, requestOptions, signal) => {
const exit = await runPromiseExit(description.invoke(input, requestOptions), signal === void 0 ? void 0 : { signal });
if (Exit.isFailure(exit)) throw description.executionError(operation, exit.cause);
return operation !== "mutation" && exit.value === void 0 ? null : exit.value;
};
const prepareQuery = (description, input, operationKey, keyEncoder, identity = []) => {
if (description.input._tag === "Inputless") return {
input: void 0,
key: identity.length === 0 ? operationKey : freezeKey([...operationKey, ...identity])
};
const reused = readPreparedInput(description.input, input);
const prepared = reused ?? description.input.prepare(input, keyEncoder);
try {
return {
input: prepared.input,
key: freezeKey([
...operationKey,
reused === void 0 ? canonicalize(prepared.keyValue) : reused.keyValue,
...identity
])
};
} catch (error) {
throw description.input.invalidKey(error);
}
};
const prepareQueryOptions = (description, argument) => {
if (argument !== void 0 && !Predicate.isObject(argument)) throw new TypeError("Query options must be an object");
const options = { ...argument };
for (const option of ["queryKeyHashFn", "queryHash"]) if (Object.hasOwn(options, option)) throw description.unsupportedQueryHash(option);
const { input } = options;
delete options["input"];
return {
input,
options,
requestOptions: description.takeOptions(options)
};
};
const finalizeQueryOptions = (options, queryKey, queryFn) => ({
...options,
queryFn,
queryKey
});
const createQueryBuilders = (description, operationKey, keyEncoder, prepareOptions, prepareIdentity) => ({
key: (input, policy) => {
const options = description.input._tag === "Inputless" ? input : policy;
const identity = prepareIdentity?.(Predicate.isObject(options) ? { ...options } : {}) ?? [];
return prepareQuery(description, input, operationKey, keyEncoder, identity).key;
},
options: (argument) => {
const { input, options, requestOptions } = prepareQueryOptions(description, argument);
const { identity, makeQuery } = prepareOptions(options, requestOptions, input);
if (description.input._tag !== "Inputless" && input === skipToken$1) return finalizeQueryOptions(options, operationKey, skipToken$1);
const prepared = prepareQuery(description, input, operationKey, keyEncoder, identity);
return finalizeQueryOptions(options, prepared.key, makeQuery(prepared.input));
}
});
const createInfiniteBuilders = (description, operationKey, keyEncoder, runPromiseExit) => {
const infiniteOperationKey = freezeKey([...operationKey, "infinite"]);
const infiniteKey = (input) => prepareQuery(description, input, infiniteOperationKey, keyEncoder).key;
const infiniteOptions = (argument) => {
const { input, options, requestOptions } = prepareQueryOptions(description, argument);
if (description.input._tag !== "Inputless" && input === skipToken$1) return finalizeQueryOptions(options, infiniteOperationKey, skipToken$1);
const { initialPageParam } = options;
const inputForPage = description.input._tag === "Inputless" ? () => void 0 : input;
if (!Predicate.isFunction(inputForPage)) throw new TypeError("Infinite query input must be a page input function");
const pageInput = inputForPage;
const initialInput = pageInput(initialPageParam);
const prepared = prepareQuery(description, initialInput, infiniteOperationKey, keyEncoder);
return finalizeQueryOptions(options, prepared.key, async ({ pageParam, signal }) => {
const pageRequest = pageInput(pageParam);
const executionInput = description.input._tag === "Input" ? description.input.pageInput(pageRequest) : void 0;
return execute(description, "infinite", executionInput, runPromiseExit, requestOptions, signal);
});
};
return {
infiniteKey,
infiniteOptions
};
};
const createMutationBuilders = (description, operationKey, runPromiseExit) => {
const mutationKey = freezeKey([...operationKey, "mutation"]);
const mutationOptions = (argument = {}) => {
const options = { ...argument };
const requestOptions = description.takeOptions(options);
return {
...options,
mutationFn: async (variables) => execute(description, "mutation", variables, runPromiseExit, requestOptions),
mutationKey
};
};
return {
mutationKey: () => mutationKey,
mutationOptions
};
};
const createUnaryBuilders = (description, operationKey, keyEncoder, runPromiseExit) => {
const queryOperationKey = freezeKey([...operationKey, "query"]);
const query = createQueryBuilders(description, queryOperationKey, keyEncoder, (_options, requestOptions) => ({
identity: [],
makeQuery: (input) => async ({ signal }) => execute(description, "query", input, runPromiseExit, requestOptions, signal)
}));
const { metadata } = description;
const metadataQuery = metadata === void 0 ? void 0 : createQueryBuilders(description, freezeKey([...operationKey, "metadata"]), keyEncoder, (options, requestOptions, input) => ({
identity: [],
makeQuery: prepareMetadataExecution(metadata, options, runPromiseExit, requestOptions, description.input._tag !== "Inputless" && input === skipToken$1)
}));
return {
...createInfiniteBuilders(description, operationKey, keyEncoder, runPromiseExit),
...createMutationBuilders(description, operationKey, runPromiseExit),
queryKey: query.key,
queryOptions: query.options,
...metadataQuery === void 0 ? void 0 : {
metadataKey: metadataQuery.key,
metadataOptions: metadataQuery.options
}
};
};
const createStreamingBuilders = (description, operationKey, keyEncoder, runPromiseExit) => {
const createView = (operation) => {
const prepare = (options, requestOptions) => description.prepareStream(options, operation, runPromiseExit, requestOptions);
return createQueryBuilders(description, freezeKey([...operationKey, operation]), keyEncoder, prepare, (options) => prepare(options).identity);
};
const live = createView("live");
const streamed = createView("streamed");
return {
liveKey: live.key,
liveOptions: live.options,
streamedKey: streamed.key,
streamedOptions: streamed.options
};
};
const deepFreezeTree = (value) => {
for (const child of Object.values(value)) if (Predicate.isObject(child)) deepFreezeTree(child);
return Object.freeze(value);
};
const validateKeyEncoders = (operations, keyEncoders, errors) => {
const supportedIds = new Set(operations.filter((operation) => operation.kind !== "Mutation" && operation.input._tag !== "Inputless").map((operation) => operation.id));
for (const id of keyEncoders.keys()) if (!supportedIds.has(id)) throw errors.unknownEncoder(id);
for (const operation of operations) if (operation.kind !== "Mutation" && operation.input._tag === "Input" && operation.input.requiresEncoder && !Predicate.isFunction(keyEncoders.get(operation.id))) throw errors.missingEncoder(operation.id);
};
const insertLeaf = (tree, prefix, path, keyEncoder, runPromiseExit) => {
const { segments } = path;
const operation = path.operation.kind === "Mutation" ? path.operation : {
...path.operation,
input: { ...path.operation.input }
};
let branch = tree;
for (let index = 0; index < segments.length - 1; index += 1) {
const segment = segments[index];
if (segment === void 0) throw new TypeError("Validated operation paths must have defined segments");
const candidate = Object.hasOwn(branch, segment) ? branch[segment] : void 0;
let child = Predicate.isObject(candidate) ? candidate : void 0;
if (child === void 0) {
const key = freezeKey([...prefix, ...segments.slice(0, index + 1)]);
child = { key: () => key };
branch[segment] = child;
}
branch = child;
}
const leafName = segments.at(-1);
if (leafName === void 0) throw new TypeError("Validated operation paths must have a leaf");
const operationKey = freezeKey([...prefix, ...segments]);
let builders;
if (operation.kind === "Mutation") builders = createMutationBuilders(operation, operationKey, runPromiseExit);
else if (operation.kind === "Streaming") builders = createStreamingBuilders(operation, operationKey, keyEncoder, runPromiseExit);
else builders = createUnaryBuilders(operation, operationKey, keyEncoder, runPromiseExit);
const leaf = {
key: () => operationKey,
...builders
};
if (operation.kind !== "Mutation") registerInputOwner(leaf.key, operation.input, keyEncoder);
branch[leafName] = leaf;
};
const createTree = (prefix, paths, keyEncoders, runPromiseExit) => {
const tree = { key: () => prefix };
for (const path of paths) insertLeaf(tree, prefix, path, keyEncoders.get(path.operation.id), runPromiseExit);
return deepFreezeTree(tree);
};
const createUtilityTree = (operations, options) => {
const prefix = freezeKey([...normalizePrefix(options.keyPrefix, options.errors), ...options.keyNamespace]);
const paths = planPaths(operations, options.errors);
validateKeyEncoders(operations, options.keyEncoders, options.errors);
const runner = options.runPromiseExit ?? Effect.runPromiseExit;
return createTree(prefix, paths, options.keyEncoders, runner);
};
//#endregion
//#region src/core/schema-key.ts
const safeAsts = /* @__PURE__ */ new WeakSet();
const encodings = /* @__PURE__ */ new WeakMap();
const inspectKeyEncoding = (value, seen) => {
if (!Predicate.isObjectOrArray(value) || seen.has(value) || safeAsts.has(value)) return "Safe";
seen.add(value);
if (Predicate.hasProperty(value, "_tag") && value._tag === "Middleware") return !Predicate.hasProperty(value, "encode") || value.encode !== Function.identity ? "RequiresEncoder" : "Safe";
if (SchemaAST.isAST(value)) {
const representation = value.annotations?.["representation"];
if (Predicate.hasProperty(representation, "id") && representation.id === "effect/schema/Redacted") return "RequiresEncoder";
if (SchemaAST.isSuspend(value)) try {
const analysis = inspectKeyEncoding(value.thunk(), seen);
if (analysis !== "Safe") return analysis;
} catch {
return "Retry";
}
}
for (const child of Object.values(value)) {
const analysis = inspectKeyEncoding(child, seen);
if (analysis !== "Safe") return analysis;
}
return "Safe";
};
const createSchemaKeyEncoding = (schema) => {
const cached = encodings.get(schema);
if (cached !== void 0) return cached;
const seen = /* @__PURE__ */ new Set();
const analysis = inspectKeyEncoding(schema.ast, seen);
let defaultEncode;
const encoding = {
requiresEncoder: analysis !== "Safe",
cacheable: analysis !== "Retry",
encode: (input) => {
if (defaultEncode === void 0) {
const encodingSchema = schema;
defaultEncode = Schema.encodeUnknownSync(encodingSchema);
}
return defaultEncode(input);
}
};
if (analysis === "Safe") {
for (const node of seen) if (SchemaAST.isAST(node)) safeAsts.add(node);
}
if (encoding.cacheable) encodings.set(schema, encoding);
return encoding;
};
//#endregion
//#region src/rpc/errors.ts
/**
* Wraps a failed RPC Exit for TanStack while preserving its complete Effect Cause.
*
* Runner rejections pass through unchanged; this class wraps failed Exits.
*/
var EffectRpcQueryError = class extends Error {
/** Identifies this error without relying on `instanceof`. */
_tag = "EffectRpcQueryError";
/** The literal tag of the RPC that failed. */
rpcTag;
/** The operation that ran the RPC. */
operation;
/** The original Cause returned by the Effect runner. */
cause;
constructor(rpcTag, operation, cause) {
super(`RPC ${rpcTag} ${operation} failed`, { cause });
this.name = "EffectRpcQueryError";
this.rpcTag = rpcTag;
this.operation = operation;
this.cause = cause;
}
};
/** Reports a live RPC stream that completed before emitting its first value. */
var EffectRpcQueryEmptyStreamError = class extends Error {
/** Identifies this error without relying on `instanceof`. */
_tag = "EffectRpcQueryEmptyStreamError";
/** The streaming RPC that completed empty. */
rpcTag;
constructor(rpcTag) {
super(`RPC ${rpcTag} live stream completed without a value`);
this.name = "EffectRpcQueryEmptyStreamError";
this.rpcTag = rpcTag;
}
};
/** Reports invalid factory, builder, or stream cache configuration. */
var EffectRpcQueryConfigError = class extends Error {
/** Identifies this error without relying on `instanceof`. */
_tag = "EffectRpcQueryConfigError";
/** A stable, machine-readable description of the failure. */
code;
/** The underlying failure, when one exists. */
cause;
/** The existing projected path associated with a collision. */
path;
/** The RPC or encoder tag associated with the invalid configuration. */
rpcTag;
constructor(code, message, options = {}) {
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
this.name = "EffectRpcQueryConfigError";
this.code = code;
this.cause = options.cause;
this.path = options.path;
this.rpcTag = options.rpcTag;
}
};
/** Reports synchronous payload construction, encoding, or canonicalization failures. */
var EffectRpcQueryKeyError = class extends Error {
/** Identifies this error without relying on `instanceof`. */
_tag = "EffectRpcQueryKeyError";
/** A stable, machine-readable description of the failure. */
code;
/** The RPC whose query key could not be prepared. */
rpcTag;
/** The underlying failure, when one exists. */
cause;
constructor(code, rpcTag, message, cause) {
super(message, cause === void 0 ? void 0 : { cause });
this.name = "EffectRpcQueryKeyError";
this.code = code;
this.rpcTag = rpcTag;
this.cause = cause;
}
};
/** Returns whether a value is an RPC execution error from this JavaScript realm. */
const isEffectRpcQueryError = (value) => value instanceof EffectRpcQueryError;
//#endregion
//#region src/core/stream-hash-index.ts
const indexes = /* @__PURE__ */ new WeakMap();
const cacheIndex = (cache) => {
const existing = indexes.get(cache);
if (existing !== void 0) return existing;
const index = {
queries: /* @__PURE__ */ new Map(),
groups: /* @__PURE__ */ new Map()
};
let nextOrder = 0;
const remove = (queryHash) => {
const previous = index.queries.get(queryHash);
if (previous === void 0) return;
const group = index.groups.get(previous.hash);
group?.delete(queryHash);
if (group?.size === 0) index.groups.delete(previous.hash);
index.queries.delete(queryHash);
};
const update = (query) => {
const previous = index.queries.get(query.queryHash);
const hash = query.options.queryKeyHashFn;
if (previous?.query === query && previous.hash === hash) return;
remove(query.queryHash);
const entry = {
query,
hash,
order: previous?.query === query ? previous.order : nextOrder
};
if (previous?.query !== query) nextOrder += 1;
index.queries.set(query.queryHash, entry);
let group = index.groups.get(hash);
if (group === void 0) {
group = /* @__PURE__ */ new Map();
index.groups.set(hash, group);
}
group.set(query.queryHash, entry);
};
for (const query of cache.getAll()) update(query);
const unsubscribe = cache.subscribe((event) => {
const query = cache.get(event.query.queryHash);
if (query === void 0) remove(event.query.queryHash);
else update(query);
if (index.queries.size === 0) {
unsubscribe();
indexes.delete(cache);
}
});
indexes.set(cache, index);
return index;
};
const validateStreamHash = ({ client, queryKey }, unsupportedHash) => {
const { queryHash, queryKeyHashFn } = client.defaultQueryOptions({ queryKey });
if (queryHash !== (queryKeyHashFn ?? hashKey)(queryKey)) throw unsupportedHash();
const cache = client.getQueryCache();
const index = cacheIndex(cache);
const current = cache.get(queryHash);
const currentOrder = current === void 0 ? Infinity : index.queries.get(current.queryHash)?.order ?? Infinity;
let canonical;
for (const [hash, queries] of index.groups) {
if (hash === queryKeyHashFn) continue;
let candidate;
try {
candidate = queries.get((hash ?? hashKey)(queryKey));
} catch (error) {
for (const entry of queries.values()) if (entry.order < currentOrder) throw error;
continue;
}
if (candidate === void 0 || candidate.query === current) continue;
if (candidate.order < currentOrder) throw unsupportedHash();
try {
canonical ??= hashKey(queryKey);
if (hashKey(candidate.query.queryKey) !== canonical) continue;
} catch {
continue;
}
throw unsupportedHash();
}
return current;
};
//#endregion
//#region src/core/stream-publication.ts
const streamPublicationBatches = async function* streamPublicationBatches(source, batchSize, signal, maxWaitMs) {
const iterator = source[Symbol.asyncIterator]();
let first = true;
let pending = [];
let pull;
let timer;
let deadline;
let closing;
const close = async () => {
closing ??= (async () => {
await iterator.return?.();
})();
await closing;
};
const onAbort = () => {
close().catch(() => {});
};
signal.addEventListener("abort", onAbort, { once: true });
const clearDeadline = () => {
clearTimeout(timer);
timer = void 0;
deadline = void 0;
};
const pullNext = async () => {
try {
return {
kind: "value",
result: await iterator.next()
};
} catch (error) {
return {
kind: "error",
error
};
}
};
const startDeadline = (milliseconds) => {
deadline = new Promise((resolve) => {
timer = setTimeout(() => {
resolve("deadline");
}, milliseconds);
});
};
try {
while (!signal.aborted) {
pull ??= pullNext();
const result = await (deadline === void 0 ? pull : Promise.race([pull, deadline]));
if (result !== "deadline" && result.kind === "error") throw result.error;
if (signal.aborted) return;
if (result === "deadline") {
const batch = pending;
pending = [];
clearDeadline();
yield batch;
continue;
}
pull = void 0;
if (result.result.done === true) break;
pending.push(result.result.value);
if (first || pending.length === batchSize) {
first = false;
const batch = pending;
pending = [];
clearDeadline();
yield batch;
} else if (deadline === void 0 && maxWaitMs !== void 0) startDeadline(maxWaitMs);
}
} catch (error) {
clearDeadline();
if (pending.length > 0 && !signal.aborted) {
yield pending;
pending = [];
}
throw error;
} finally {
clearDeadline();
signal.removeEventListener("abort", onAbort);
await close();
const remaining = await pull;
if (remaining?.kind === "error") throw remaining.error;
}
if (pending.length > 0 && !signal.aborted) yield pending;
};
//#endregion
//#region src/core/streamed-query.ts
const validatePublicationDeadline = (publicationMaxWaitMs) => {
if (publicationMaxWaitMs !== void 0 && (!Predicate.isNumber(publicationMaxWaitMs) || !Number.isSafeInteger(publicationMaxWaitMs) || publicationMaxWaitMs <= 0 || publicationMaxWaitMs > 2147483647)) throw new RangeError("publicationMaxWaitMs must be an integer from 1 to 2147483647");
};
const prepareStreamPolicy = (options, operation, invalid) => {
let policy;
if (operation === "streamed") {
const { maxChunks, refetchMode = "reset", publicationBatchSize = 1, publicationMaxWaitMs } = options;
if (maxChunks !== "unbounded" && (!Predicate.isNumber(maxChunks) || !Number.isSafeInteger(maxChunks) || maxChunks <= 0)) throw invalid("InvalidMaxChunks", "maxChunks must be a positive safe integer or 'unbounded'");
if (refetchMode !== "reset" && refetchMode !== "append" && refetchMode !== "replace") throw invalid("InvalidRefetchMode", "refetchMode must be reset, append, or replace");
if (!Predicate.isNumber(publicationBatchSize) || !Number.isSafeInteger(publicationBatchSize) || publicationBatchSize <= 0) throw new RangeError("publicationBatchSize must be a positive safe integer");
validatePublicationDeadline(publicationMaxWaitMs);
policy = {
_tag: "Accumulated",
maxChunks: maxChunks === "unbounded" ? void 0 : maxChunks,
refetchMode,
publicationBatchSize,
publicationMaxWaitMs
};
} else policy = { _tag: "Live" };
delete options["refetchMode"];
delete options["maxChunks"];
delete options["publicationBatchSize"];
delete options["publicationMaxWaitMs"];
return {
policy,
history: policy._tag === "Live" ? void 0 : {
maxChunks: policy.maxChunks ?? null,
refetchMode: policy.refetchMode,
...policy.publicationBatchSize === 1 ? void 0 : { publicationBatchSize: policy.publicationBatchSize },
...policy.publicationMaxWaitMs === void 0 ? void 0 : { publicationMaxWaitMs: policy.publicationMaxWaitMs }
}
};
};
const streamQueryIterable = (source, signal, emptyError) => ({ [Symbol.asyncIterator]() {
const iterator = source[Symbol.asyncIterator]();
let closePromise;
let emitted = false;
const detach = () => {
signal.removeEventListener("abort", onAbort);
};
const close = async () => {
closePromise ??= (async () => {
try {
return await iterator.return?.() ?? {
done: true,
value: void 0
};
} finally {
detach();
}
})();
return await closePromise;
};
const onAbort = () => {
Promise.resolve().then(close).catch(detach);
};
signal.addEventListener("abort", onAbort, { once: true });
return {
async next() {
let result;
if (signal.aborted) result = await close();
else try {
result = await iterator.next();
} catch (error) {
detach();
throw error;
}
if (result.done === true) {
detach();
if (!emitted && emptyError !== void 0) throw emptyError();
}
emitted = true;
return result;
},
return: close,
async throw(cause) {
detach();
if (iterator.throw !== void 0) return await iterator.throw(cause);
await close();
throw cause;
}
};
} });
const makeStreamQuery = ({ policy, source, executionError, unsupportedHash, emptyError, runPromiseExit }) => {
const validateHash = (context) => validateStreamHash(context, unsupportedHash);
const streamFn = async ({ signal }) => {
const stream = Stream.unwrap(Effect.gen(function* consumeStream() {
const scope = yield* Scope.make();
yield* Effect.addFinalizer((exit) => Scope.close(scope, exit).pipe(Effect.catchCause((cause) => Effect.die(executionError(cause)))));
const pull = yield* Stream.toPull(source).pipe(Scope.provide(scope));
return Stream.fromPull(Effect.succeed(Effect.uninterruptibleMask((restore) => restore(pull).pipe(Effect.onError((cause) => Scope.close(scope, Pull.doneExitFromCause(cause))), Effect.catchCause((cause) => {
const completion = Pull.filterDone(cause);
if (Result.isFailure(completion) && (!signal.aborted || !Cause.hasInterruptsOnly(completion.failure))) return Effect.fail(executionError(completion.failure));
return Effect.failCause(cause);
})))));
}));
const exit = await runPromiseExit(Stream.toAsyncIterableEffect(stream), { signal });
if (Exit.isFailure(exit)) throw executionError(exit.cause);
return streamQueryIterable(exit.value, signal, policy._tag === "Live" ? emptyError : void 0);
};
if (policy._tag === "Live") {
const queryFn = experimental_streamedQuery({
initialValue: null,
reducer: (_latest, value) => value === void 0 ? null : value,
streamFn
});
return async (context) => {
validateHash(context);
return await queryFn(context);
};
}
const { maxChunks, refetchMode, publicationBatchSize, publicationMaxWaitMs } = policy;
return async (context) => {
const query = validateHash(context);
const reset = refetchMode === "reset" && query?.isFetched() === true;
let emitted = false;
const result = await experimental_streamedQuery({
initialValue: [],
reducer: (values, batch) => {
const history = reset && !emitted ? [] : values;
emitted = true;
if (maxChunks === void 0) return [...history, ...batch];
const start = Math.max(0, history.length + batch.length - maxChunks);
const next = new Array(history.length + batch.length - start);
for (let index = start; index < history.length + batch.length; index += 1) next[index - start] = index < history.length ? history[index] : batch[index - history.length];
return next;
},
refetchMode,
streamFn: async (streamContext) => streamPublicationBatches(await streamFn(streamContext), publicationBatchSize, streamContext.signal, publicationMaxWaitMs)
})(context);
return reset && !emitted ? [] : result;
};
};
//#endregion
//#region src/rpc/streamed-query.ts
const createStreamPreparation = (rpc) => (options, operation, runPromiseExit, requestOptions) => {
const { policy, history } = prepareStreamPolicy(options, operation, (code, message) => new EffectRpcQueryConfigError(code, message, { rpcTag: rpc.tag }));
return {
identity: history === void 0 ? [] : [Object.freeze(history)],
makeQuery: (input) => makeStreamQuery({
policy,
source: Stream.suspend(() => rpc.invoke(input, requestOptions)),
executionError: (cause) => new EffectRpcQueryError(rpc.tag, operation, cause),
unsupportedHash: () => new EffectRpcQueryConfigError("UnsupportedQueryHash", "Stream queries require consistent QueryClient hashing", { rpcTag: rpc.tag }),
emptyError: () => new EffectRpcQueryEmptyStreamError(rpc.tag),
runPromiseExit
})
};
};
//#endregion
//#region src/rpc/operation.ts
const rpcInputs = /* @__PURE__ */ new WeakMap();
const compileRpcInput = (definition) => {
const { payloadSchema, _tag: rpcTag } = definition;
if (SchemaAST.isVoid(payloadSchema.ast)) return { _tag: "Inputless" };
const keyEncoding = createSchemaKeyEncoding(payloadSchema);
return {
_tag: "Input",
requiresEncoder: keyEncoding.requiresEncoder,
cacheable: keyEncoding.cacheable,
pageInput: (input) => payloadSchema.make(input),
invalidKey: (cause) => new EffectRpcQueryKeyError("InvalidKeyValue", rpcTag, `The key payload for RPC ${rpcTag} is not JSON-safe`, cause),
prepare: (input, encoder) => {
let normalized;
try {
normalized = payloadSchema.make(input);
} catch (error) {
throw new EffectRpcQueryKeyError("PayloadConstructionFailed", rpcTag, `Could not construct the payload for RPC ${rpcTag}`, error);
}
let keyValue;
try {
keyValue = encoder ? encoder(normalized) : keyEncoding.encode(normalized);
} catch (error) {
throw new EffectRpcQueryKeyError(encoder ? "KeyEncoderFailed" : "PayloadEncodingFailed", rpcTag, `Could not encode the key payload for RPC ${rpcTag}`, error);
}
return {
input: normalized,
keyValue
};
}
};
};
const createRpcInput = (definition) => {
let input = rpcInputs.get(definition);
if (input === void 0) {
input = compileRpcInput(definition);
if (input._tag === "Inputless" || input.cacheable) rpcInputs.set(definition, input);
}
return input;
};
const takeRpcOptions = (options) => {
const { rpcOptions } = options;
delete options["rpcOptions"];
return rpcOptions;
};
const describeRpc = (definition, client) => {
const rpcTag = definition._tag;
const identity = {
id: rpcTag,
path: rpcTag.split("."),
input: createRpcInput(definition),
unsupportedQueryHash: (option) => new EffectRpcQueryConfigError("UnsupportedQueryHash", `${option} must be configured through QueryClient defaults`, { rpcTag }),
takeOptions: takeRpcOptions
};
if (!RpcSchema.isStreamSchema(definition.successSchema)) {
const invoke = client;
return {
...identity,
kind: "Unary",
invoke: (input, options) => invoke(rpcTag, input, options),
executionError: (operation, cause) => new EffectRpcQueryError(rpcTag, operation, cause)
};
}
const invoke = client;
return {
...identity,
kind: "Streaming",
prepareStream: createStreamPreparation({
tag: rpcTag,
invoke: (input, options) => invoke(rpcTag, input, options)
})
};
};
const extractRpcs = (group, client) => {
if (!Predicate.hasProperty(group, "requests") || !Predicate.isMap(group.requests)) throw new TypeError("Expected an Effect RPC group with a request map");
return Array.from(group.requests.values(), (value) => {
if (!Rpc.isRpc(value)) throw new TypeError("Expected an Effect RPC declaration");
return describeRpc(value, client);
});
};
const rpcTreeErrors = {
invalidPrefix: (reason, cause) => new EffectRpcQueryConfigError("InvalidKeyPrefix", reason === "Shape" ? "keyPrefix must be a non-empty readonly tuple of JSON-safe values" : "keyPrefix must contain only JSON-safe values", { cause }),
invalidPath: (rpcTag) => new EffectRpcQueryConfigError("InvalidRpcPath", `RPC tag ${rpcTag} cannot be projected into a utility path`, { rpcTag }),
pathCollision: (rpcTag, segments, relation) => {
const path = segments.join(".");
return new EffectRpcQueryConfigError("RpcPathCollision", `RPC tag ${rpcTag} ${relation} utility path ${path}`, {
path,
rpcTag
});
},
unknownEncoder: (rpcTag) => new EffectRpcQueryConfigError("UnknownKeyEncoder", `No payload-bearing RPC exists for key encoder ${rpcTag}`, { rpcTag }),
missingEncoder: (rpcTag) => new EffectRpcQueryConfigError("MissingKeyEncoder", `RPC ${rpcTag} requires a safe custom key encoder`, { rpcTag })
};
//#endregion
//#region src/rpc/create-utils.ts
/**
* Derives an eager, frozen TanStack Query utility tree from an Effect RPC group.
*
* Dotted RPC tags become nested properties with unary or streaming utility leaves.
* The caller retains ownership of the ready client's Scope and lifecycle.
*
* @throws {@link EffectRpcQueryConfigError} if the prefix, paths, or encoders are invalid.
*/
const createRpcQueryUtils = (group, options) => {
const rpcs = extractRpcs(group, options.client);
const runPromiseExit = options.runPromiseExit;
return createUtilityTree(rpcs, {
keyPrefix: options.keyPrefix,
keyNamespace: ["rpc"],
keyEncoders: new Map(Object.entries(options.keyEncoders ?? {})),
runPromiseExit,
errors: rpcTreeErrors
});
};
//#endregion
//#region src/http/errors.ts
/** Preserves the complete failed Exit Cause with declaration-only metadata. */
var EffectHttpApiQueryError = class extends Error {
_tag = "EffectHttpApiQueryError";
apiId;
groupId;
endpoint;
method;
operation;
cause;
constructor(identity, operation, cause) {
super(`HTTP ${identity.method} ${identity.groupId}/${identity.endpoint} ${operation} failed`, { cause });
this.name = "EffectHttpApiQueryError";
this.apiId = identity.apiId;
this.groupId = identity.groupId;
this.endpoint = identity.endpoint;
this.method = identity.method;
this.operation = operation;
this.cause = cause;
}
};
var EffectHttpApiQueryEmptyStreamError = class extends Error {
_tag = "EffectHttpApiQueryEmptyStreamError";
apiId;
groupId;
endpoint;
method;
operation = "live";
constructor(identity) {
super(`HTTP ${identity.method} ${identity.groupId}/${identity.endpoint} live stream completed without a value`);
this.name = "EffectHttpApiQueryEmptyStreamError";
this.apiId = identity.apiId;
this.groupId = identity.groupId;
this.endpoint = identity.endpoint;
this.method = identity.method;
}
};
/** Reports invalid factory, builder, or stream cache configuration. */
var EffectHttpApiQueryConfigError = class extends Error {
_tag = "EffectHttpApiQueryConfigError";
code;
apiId;
groupId;
endpoint;
method;
path;
cause;
constructor(code, message, options) {
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
this.name = "EffectHttpApiQueryConfigError";
this.code = code;
this.apiId = options.apiId;
this.groupId = options.groupId;
this.endpoint = options.endpoint;
this.method = options.method;
this.path = options.path;
this.cause = options.cause;
}
};
/** Reports synchronous request encoding and key canonicalization failures. */
var EffectHttpApiQueryKeyError = class extends Error {
_tag = "EffectHttpApiQueryKeyError";
code;
apiId;
groupId;
endpoint;
method;
cause;
constructor(code, identity, message, cause) {
super(message, cause === void 0 ? void 0 : { cause });
this.name = "EffectHttpApiQueryKeyError";
this.code = code;
this.apiId = identity.apiId;
this.groupId = identity.groupId;
this.endpoint = identity.endpoint;
this.method = identity.method;
this.cause = cause;
}
};
/** Recognizes HTTP execution errors from this JavaScript realm. */
const isEffectHttpApiQueryError = (value) => value instanceof EffectHttpApiQueryError;
//#endregion
//#region src/http/request.ts
const multipartBrands = (schema) => {
const brands = /* @__PURE__ */ new Set();
const visited = /* @__PURE__ */ new WeakSet();
let current = schema;
while (Schema.isSchema(current) && !visited.has(current)) {
visited.add(current);
if (Predicate.hasProperty(current, "identifier") && (current.identifier === HttpApiSchema.MultipartTypeId || current.identifier === HttpApiSchema.MultipartStreamTypeId)) brands.add(current.identifier);
current = Predicate.hasProperty(current, "schema") ? current.schema : void 0;
}
return brands;
};
const classifyEndpoint = (endpoint, identity) => {
const payloads = [];
let multipart = false;
let multipartStream = false;
for (const { encoding, schemas } of endpoint.payload.values()) for (const schema of schemas) {
const brands = multipartBrands(schema);
const buffered = brands.has(HttpApiSchema.MultipartTypeId);
const streamed = brands.has(HttpApiSchema.MultipartStreamTypeId);
const expectedBuffered = encoding._tag === "Multipart" && encoding.mode === "buffered";
const expectedStreamed = encoding._tag === "Multipart" && encoding.mode === "stream";
if (!(buffered === expectedBuffered && streamed === expectedStreamed)) throw new EffectHttpApiQueryConfigError("UnsupportedEndpointMetadata", `HTTP endpoint ${identity.groupId}/${identity.endpoint} has contradictory multipart metadata`, identity);
multipart ||= expectedBuffered;
multipartStream ||= expectedStreamed;
payloads.push(schema);
}
if (multipartStream) return;
const successes = [...endpoint.success];
if (successes.filter((schema) => Predicate.hasProperty(HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema, "~effect/http-api/HttpApiSchema/Stream")).length > 0) {
const [success] = successes;
const body = HttpApiSchema.isWithHeaders(success) ? success.schema : success;
if (multipart || successes.length !== 1 || !Predicate.hasProperty(body, "_tag") || body._tag !== "StreamSse") return;
return {
kind: "Streaming",
schemas: payloads
};
}
return multipart ? { kind: "Mutation" } : {
kind: "Unary",
schemas: payloads
};
};
const omitUndefined = (value, seen = /* @__PURE__ */ new WeakSet()) => {
if (!Predicate.isObjectOrArray(value)) return value;
if (seen.has(value)) throw new TypeError("Key values must not contain cycles");
seen.add(value);
let result = value;
if (Array.isArray(value)) {
let copy;
for (let index = 0; index < value.length; index += 1) {
if (!Object.hasOwn(value, index)) throw new TypeError("Key values must not contain sparse arrays");
const item = value[index];
const next = omitUndefined(item, seen);
if (copy !== void 0) copy.push(next);
else if (next !== item) {
copy = Array.from({ length: index }, (_, offset) => value[offset]);
copy.push(next);
}
}
result = copy ?? value;
} else if (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) {
let copy;
const entries = Object.entries(value);
for (const [index, [name, item]] of entries.entries()) if (item === void 0) copy ??= entries.slice(0, index);
else {
const next = omitUndefined(item, seen);
if (next !== item) copy ??= entries.slice(0, index);
copy?.push([name, next]);
}
result = copy === void 0 ? value : Object.fromEntries(copy);
}
seen.delete(value);
return result;
};
const normalizeRequestKey = (value) => {
const request = omitUndefined(value);
if (!Predicate.isObject(request)) throw new TypeError("Encoded HTTP requests must be objects");
if (Predicate.isObject(request["headers"])) {
const headers = Object.create(null);
for (const [name, headerValue] of Object.entries(request["headers"])) {
const lower = name.toLowerCase();
if (Object.hasOwn(headers, lower) && JSON.stringify(headers[lower]) !== JSON.stringify(headerValue)) throw new TypeError("Encoded header names must not have conflicting values");
headers[lower] = headerValue;
}
return {
...request,
headers
};
}
return request;
};
const createHttpRequest = (endpoint, identity) => {
const classified = classifyEndpoint(endpoint, identity);
if (classified === void 0 || classified.kind === "Mutation") return classified;
const payloads = classified.schemas;
const fields = {};
if (endpoint.params !== void 0) fields["params"] = endpoint.params;
if (endpoint.query !== void 0) fields["query"] = endpoint.query;
if (endpoint.headers !== void 0) fields["headers"] = endpoint.headers;
if (payloads.length > 0) fields["payload"] = Schema.Union(payloads);
if (Object.keys(fields).length === 0) return {
kind: classified.kind,
input: { _tag: "Inputless" }
};
const schema = Schema.Struct(fields);
const keyEncoding = createSchemaKeyEncoding(schema);
const invalidKey = (cause) => new EffectHttpApiQueryKeyError("InvalidKeyValue", identity, `The HTTP key for ${identity.groupId}/${identity.endpoint} is not JSON-safe`, cause);
return {
kind: classified.kind,
input: {
_tag: "Input",
requiresEncoder: payloads.length > 1 || keyEncoding.requiresEncoder,
cacheable: keyEncoding.cacheable,
pageInput: (input) => input,
invalidKey,
prepare: (input, encoder) => {
let keyValue;
try {
keyValue = encoder ? encoder(input) : keyEncoding.encode(input);
} catch (error) {
throw new EffectHttpApiQueryKeyError(encoder ? "KeyEncoderFailed" : "RequestEncodingFailed", identity, `Could not encode the HTTP key for ${identity.groupId}/${identity.endpoint}`, error);
}
try {
return {
input,
keyValue: encoder ? keyValue : normalizeRequestKey(keyValue)
};
} catch (error) {
throw invalidKey(error);
}
}
}
};
};
//#endregion
//#region src/http/streamed-query.ts
const decoderOptions = (identity, options) => {
const { sseOptions } = options;
const decoder = Predicate.isObject(sseOptions) ? sseOptions : void 0;
const maxEventSize = decoder?.["maxEventSize"];
const dispatchEmptyNamedEvents = decoder?.["dispatchEmptyNamedEvents"];
const limit = maxEventSize === void 0 ? 10485760 : maxEventSize;
if (!Predicate.isNumber(limit) || !Number.isSafeInteger(limit) || limit <= 0) throw new EffectHttpApiQueryConfigError("InvalidMaxEventSize", "sseOptions.maxEventSize must be a positive safe integer", identity);
if (dispatchEmptyNamedEvents !== void 0 && !Predicate.isBoolean(dispatchEmptyNamedEvents)) throw new EffectHttpApiQueryConfigError("InvalidSseOptions", "sseOptions.dispatchEmptyNamedEvents must be a boolean", identity);
return Object.freeze({
maxEventSize: limit,
dispatchEmptyNamedEvents: dispatchEmptyNamedEvents ?? false
});
};
const createHttpStreamPreparation = (identity, invoke) => (options, operation, runPromiseExit) => {
const { policy, history } = prepareStreamPolicy(options, operation, (code, message) => new EffectHttpApiQueryConfigError(code, message, identity));
const sseOptions = decoderOptions(identity, options);
delete options["sseOptions"];
return {
identity: [Object.freeze({
...history,
maxEventSize: sseOptions.maxEventSize,
...sseOptions.dispatchEmptyNamedEvents === true ? { dispatchEmptyNamedEvents: true } : void 0
})],
makeQuery: (input) => makeStreamQuery({
policy,
source: Stream.unwrap(Effect.suspend(() => invoke(input, sseOptions)).pipe(Effect.map((response) => {
if (Predicate.hasProperty(response, "body") && Predicate.hasProperty(response, "headers") && Stream.isStream(response.body)) return response.body.pipe(Stream.map((body) => HttpApiSchema.withHeaders({
body,
headers: response.headers
})));
if (!Stream.isStream(response)) throw new TypeError("HTTP SSE clients must return a Stream");
return response;
}))),
executionError: (cause) => new EffectHttpApiQueryError(identity, operation, cause),
unsupportedHash: () => new EffectHttpApiQueryConfigError("UnsupportedQueryHash", "Stream queries require consistent QueryClient hashing", identity),
emptyError: () => new EffectHttpApiQueryEmptyStreamError(identity),
runPromiseExit
})
};
};
//#endregion
//#region src/http/operation.ts
const freezeMetadata = (data) => {
if (!Predicate.isObject(data)) return data;
const { headers } = data;
const snapshot = Predicate.isObject(headers) && (!Object.isFrozen(headers) || Object.getPrototypeOf(headers) !== Object.prototype) ? Object.freeze({ ...headers }) : headers;
return Object.isFrozen(data) && snapshot === headers ? data : Object.freeze({
...data,
headers: snapshot
});
};
const httpContracts = /* @__PURE__ */ new WeakMap();
const describeHttpEndpoints = (api) => {
const cached = httpContracts.get(api);
if (cached !== void 0) return cached;
const descriptions = [];
for (const group of Object.values(api.groups)) for (const endpoint of Object.values(group.endpoints)) {
const identity = {
apiId: api.identifier,
groupId: group.identifier,
endpoint: endpoint.identifier,
method: endpoint.method
};
const request = createHttpRequest(endpoint, identity);
if (request === void 0) continue;
descriptions.push({
identity,
topLevel: group.topLevel,
request
});
}
if (descriptions.every(({ request }) => request.kind === "Mutation" || request.input._tag === "Inputless" || request.input.cacheable)) httpContracts.set(api, descriptions);
return descriptions;
};
const extractHttpEndpoints = (api, client) => {
const operations = [];
for (const { identity, topLevel, request } of describeHttpEndpoints(api)) {
let target = client;
if (!topLevel) target = Predicate.hasProperty(client, identity.groupId) ? client[identity.groupId] : void 0;
const invoke = (requestInput, responseMode, sseOptions) => {
const method = Predicate.hasProperty(target, identity.endpoint) ? target[identity.endpoint] : void 0;
if (method === void 0) throw new TypeError(`Missing HTTP client endpoint ${identity.endpoint}`);
return method.bind(target)({
...Predicate.isObject(requestInput) ? requestInput : void 0,
responseMode,
...sseOptions === void 0 ? void 0 : { sseOptions }
});
};
const common = {
identity,
id: JSON.stringify([identity.groupId, identity.endpoint]),
path: topLevel ? [identity.endpoint] : [identity.groupId, identity.endpoint],
unsupportedQueryHash: (option) => new EffectHttpApiQueryConfigError("UnsupportedQueryHash", `${option} must be configured through QueryClient defaults`, identity),
takeOptions: () => null
};
if (request.kind === "Streaming") operations.push({
...common,
...request,
kind: "Streaming",
prepareStream: createHttpStreamPreparation(identity, (requestInput, sseOptions) => invoke(requestInput, "decoded-only", sseOptions))
});
else operations.push({
...common,
...request,
invoke: (requestInput) => invoke(requestInput, "decoded-only"),
...request.kind === "Unary" ? { metadata: {
invoke: (requestInput) => Effect.map(invoke(requestInput, "decoded-and-response"), ([data, response]) => freezeMetadata({
data: data === void 0 ? null : data,
status: response.status,
headers: response.headers
})),
executionError: (operation, cause) => new EffectHttpApiQueryError(identity, operation, cause),
finalizeData: freezeMetadata
} } : void 0,
executionError: (operation, cause) => new EffectHttpApiQueryError(identity, operation, cause)
});
}
return operations;
};
const httpTreeErrors = (api, operations) => {
const identities = new Map(operations.map((operation) => [operation.id, operation.identity]));
const identity = (id) => identities.get(id) ?? { apiId: api.identifier };
return {
invalidPrefix: (reason, cause) => new EffectHttpApiQueryConfigError("InvalidKeyPrefix", reason === "Shape" ? "keyPrefix must be a non-empty readonly tuple of JSON-safe values" : "keyPrefix must contain only JSON-safe values", {
apiId: api.identifier,
cause
}),
invalidPath: (id) => new EffectHttpApiQueryConfigError("InvalidEndpointPath", `HTTP endpoint ${id} cannot be projected into a utility path`, identity(id)),
pathCollision: (id, path, relation) => new EffectHttpApiQueryConfigError("EndpointPathCollision", `HTTP endpoint ${id} ${relation} utility path ${JSON.stringify(path)}`, {
...identity(id),
path
}),
unknownEncoder: (id) => new EffectHttpApiQueryConfigError("UnknownKeyEncoder", `No query-enabled HTTP endpoint exists for key encoder ${id}`, identity(id)),
missingEncoder: (id) => new EffectHttpApiQueryConfigError("MissingKeyEncoder", `HTTP endpoint ${id} requires a safe custom key encoder`, identity(id))
};
};
const compileHttpOperations = (api, client, suppliedEncoders) => {
const operations = extractHttpEndpoints(api, client);
const errors = httpTreeErrors(api, operations);
const encoderGroups = new Set(operations.filter((operation) => operation.kind !== "Mutation" && operation.input._tag === "Input").map((operation) => operation.identity.groupId));
const keyEncoders = /* @__PURE__ */ new Map();
for (const [groupId, endpoints] of Object.entries(Predicate.isObject(suppliedEncoders) ? suppliedEncoders : {})) {
if (!encoderGroups.has(groupId)) throw errors.unknownEncoder(JSON.stringify([groupId]));
if (!Predicate.isObject(endpoints)) throw new TypeError("HTTP key encoders must be grouped by endpoint");
for (const [endpoint, encoder] of Object.entries(endpoints)) keyEncoders.set(JSON.stringify([groupId, endpoint]), encoder);
}
return {
operations,
errors,
keyEncoders
};
};
//#endregion
//#region src/http/create-utils.ts
const createHttpApiQueryUtils = (api, options) => {
if (!HttpApi.isHttpApi(api)) throw new TypeError("Expected an Effect HttpApi declaration");
const { operations, errors, keyEncoders } = compileHttpOperations(api, options.client, options.keyEncoders);
const runPromiseExit = options.runPromiseExit;
return createUtilityTree(operations, {
keyPrefix: options.keyPrefix,
keyNamespace: ["http", api.identifier],
keyEncoders,
runPromiseExit,
errors
});
};
//#endregion
//#region src/core/stream-snapshot.ts
const validateSnapshotControls = (options, timeoutMs) => {
if (Object.hasOwn(options, "queryHash") || Object.hasOwn(options, "queryKeyHashFn")) throw new TypeError("Stream snapshots inherit hashing from QueryClient defaults");
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) throw new RangeError("timeoutMs must be positive and at most 2147483647");
};
const fetchStreamSnapshot = async (queryClient, options, { mode = "fresh", signal, timeoutMs = 1e4 } = {}) => {
signal?.throwIfAborted();
validateSnapshotControls(options, timeoutMs);
if (!Predicate.isFunction(options.queryFn)) throw new TypeError("Stream snapshots require a callable query function");
const cache = queryClient.getQueryCache();
const { queryHash } = queryClient.defaultQueryOptions(options);
const existing = cache.get(queryHash);
if (mode === "cached" && existing?.state.status === "success") return await queryClient.query({
...options,
queryHash,
staleTime: "static"
});
if (existing !== void 0 && (existing.state.fetchStatus !== "idle" || existing.getObserversCount() > 0)) throw new Error("Stream snapshots require an idle query without observers");
let published = false;
let started = false;
let executionSignal;
let failed = false;
let failure;
let cleanup = Promise.resolve();
let cancellation;
const cancel = async () => {
cancellation ??= queryClient.cancelQueries({ predicate: (query) => query.queryHash === queryHash });
await cancellation;
};
let ready;
let rejectPublication;
const publication = new Promise((resolve, reject) => {
ready = resolve;
rejectPublication = reject;
});
const stop = cache.subscribe((event) => {
if (event.query.queryHash === queryHash && event.type === "updated" && event.action.type === "success" && event.action.manual === true) {
published = true;
ready();
cancel();
}
});
const onAbort = () => {
rejectPublication(signal?.reason);
};
signal?.addEventListener("abort", onAbort, { once: true });
const timer = setTimeout(() => {
rejectPublication(new DOMException("Stream snapshot timed out", "TimeoutError"));
}, timeoutMs);
let result;
try {
queryClient.invalidateQueries({
predicate: (query) => query.queryHash === queryHash,
refetchType: "none"
});
signal?.throwIfAborted();
const fetching = queryClient.query({
...options,
queryHash,
staleTime: 0,
initialDataUpdatedAt: 0,
queryFn: async (context) => {
started = true;
failed = false;
failure = void 0;
executionSignal = context.signal;
let finished;
cleanup = new Promise((resolve) => {
finished = resolve;
});
try {
return await options.queryFn(context);
} catch (error) {
failed = true;
failure = error;
throw error;
} finally {
finished();
}
}
});
await Promise.race([publication, fetching]);
result = await fetching;
if (!published && (!started || executionSignal?.aborted === true)) throw new CancelledError({ revert: true });
} finally {
stop();
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
await cancel();
await cleanup;
}
if (failed) throw failure;
return result;
};
//#endregion
//#region src/core/view-prefix.ts
/** Selects every input and stream policy for one leaf's query view. */
const viewPrefix = (leaf, view) => Object.freeze([...leaf.key(), view]);
//#endregion
//#region src/core/query-filter.ts
function createQueryFilter(leaf, view, encode) {
const prefix = viewPrefix(leaf, view);
if (encode === void 0) return (options) => ({
...options,
queryKey: prefix
});
return (input, options) => ({
...options,
queryKey: Object.freeze([...prefix, canonicalize(encode(input))])
});
}
//#endregion
//#region src/core/hydration.ts
const makeCodec = (prefix, schema) => {
const hash = hashKey(prefix);
return Object.freeze({
prefix,
schema,
matches: (key) => key.length >= prefix.length && hashKey(key.slice(0, prefix.length)) === hash
});
};
/** Registers a codec whose decoded type matches the generated cached representation. */
const queryViewCodec = (leaf, view, schema) => makeCodec(viewPrefix(leaf, view), schema);
/** Infinite data needs an explicit page-parameter codec in addition to its page codec. */
const infiniteQueryCodec = (leaf, codecs) => makeCodec(Object.freeze([...leaf.key(), "infinite"]), Schema.Struct({
pages: Schema.Array(codecs.page),
pageParams: Schema.Array(codecs.pageParam)
}));
/** Native synchronous callbacks for a single service-free leaf/view snapshot. */
const hydrationCallbacks = (codec) => ({
serializeData: Schema.encodeUnknownSync(codec.schema),
deserializeData: Schema.decodeUnknownSync(codec.schema)
});
var HydrationCodecError = class extends Error {
_tag = "HydrationCodecError";
code;
constructor(code) {
super({
DuplicateCodec: "A hydration codec is already registered for this leaf and view",
UnknownQuery: "No hydration codec is registered for this query",
UnsettledQuery: "Hydration codecs require successful queries without pending promises"
}[code]);
this.name = "HydrationCodecError";
this.code = code;
}
};
/** Indexes explicit leaf/view codecs without owning a QueryClient, runner, or serialization. */
const createHydrationRegistry = (entries) => {
const byLength = /* @__PURE__ */ new Map();
const codecs = entries;
for (const codec of codecs) {
const lengthCodecs = byLength.get(codec.prefix.length) ?? /* @__PURE__ */ new Map();
const hash = hashKey(codec.prefix);
if (lengthCodecs.has(hash)) throw new HydrationCodecError("DuplicateCodec");
lengthCodecs.set(hash, codec);
byLength.set(codec.prefix.length, lengthCodecs);
}
const lengths = Array$1.sort(byLength.keys(), Order.flip(Order.Number));
const codecFor = (key) => {
for (const length of lengths) if (key.length >= length) {
const codec = byLength.get(length)?.get(hashKey(key.slice(0, length)));
if (codec !== void 0) return codec;
}
};
const transform = (queries, convert) => Effect.forEach(Effect.fnUntraced(function* transformQuery(query) {
if (query.state.status !== "success" || query.promise !== void 0) return yield* Effect.fail(new HydrationCodecError("UnsettledQuery"));
const codec = codecFor(query.queryKey);
if (codec === void 0) return yield* Effect.fail(new HydrationCodecError("UnknownQuery"));
const data = yield* convert(codec, query.state.data);
return {
...query,
state: {
...query.state,
data
}
};
}))(queries);
return {
matches: (key) => codecFor(key) !== void 0,
encodeQueries: (queries) => transform(queries, (codec, data) => Schema.encodeUnknownEffect(codec.schema)(data)),
decodeQueries: (queries) => transform(queries, (codec, data) => Schema.decodeUnknownEffect(codec.schema)(data))
};
};
//#endregion
//#region src/core/stream-recovery.ts
const checkedCursor = (cursor) => {
if (!Number.isSafeInteger(cursor) || cursor < 0) throw new RangeError("Stream recovery cursors must be nonnegative safe integers");
return cursor;
};
/** Restarts a decoded stream without putting attempt-specific resume state in a query key. */
const recoverStream = (options) => Stream.suspend(() => {
let cursor = checkedCursor(options.initialCursor ?? 0);
const state = (value) => options.onState?.(value) ?? Effect.void;
const connect = (attempt) => Stream.unwrap(Effect.gen(function* connectStream() {
yield* state({
status: "connecting",
cursor,
attempt
});
const scope = yield* Scope.make();
yield* Effect.addFinalizer((exit) => Scope.close(scope, exit));
const source = Stream.suspend(() => options.connect(cursor)).pipe(Stream.rechunk(1), Stream.mapEffect(Effect.fnUntraced(function* trackCursor(value) {
const next = checkedCursor(options.cursor(value));
if (next <= cursor) return Option.none();
if (next > cursor + 1) yield* options.onGap?.(cursor, next) ?? Effect.void;
cursor = next;
yield* state({
status: "connected",
cursor,
attempt
});
return Option.some(value);
})), Stream.filter(Option.isSome), Stream.map((value) => value.value));
const pull = yield* Stream.toPull(source).pipe(Scope.provide(scope));
return Stream.fromPull(Effect.succeed(Effect.uninterruptibleMask((restore) => restore(pull).pipe(Effect.onError((cause) => Scope.close(scope, Pull.doneExitFromCause(cause))), Effect.catchCause((cause) => {
const completion = Pull.filterDone(cause);
return Effect.failCause(Result.isFailure(completion) ? completion.failure : cause);
})))));
})).pipe(Stream.catchCause((cause) => {
if (Cause.hasInterrupts(cause)) return Stream.failCause(cause);
return Stream.unwrap(Effect.gen(function* recoverConnection() {
const nextAttempt = attempt + 1;
const delayInput = options.retryDelay(cause, nextAttempt);
const expired = options.isReplayExpired?.(cause) === true;
if (delayInput === void 0 || expired && options.onReplayExpired === void 0) return yield* Effect.failCause(cause);
const delay = Duration.fromInputUnsafe(delayInput);
yield* state({
status: "backing-off",
cursor,
attempt: nextAttempt,
delay,
cause
});
yield* Effect.sleep(delay);
if (expired && options.onReplayExpired !== void 0) {
yield* state({
status: "refreshing",
cursor,
attempt: nextAttempt,
cause
});
cursor = checkedCursor(yield* options.onReplayExpired(cause));
}
return connect(nextAttempt);
}));
}));
return connect(0).pipe(Stream.onExit((exit) => {
if (Exit.isSuccess(exit)) return state({
status: "completed",
cursor
});
return state(Cause.hasInterrupts(exit.cause) ? {
status: "stopped",
cursor
} : {
status: "failed",
cursor,
cause: exit.cause
});
}));
});
//#endregion
//#region src/core/configure.ts
const isCallback = (value) => Predicate.isFunction(value);
const once = (next) => {
let called = false;
return async () => {
if (called) throw new Error("An execution interceptor may call next only once per attempt");
called = true;
return await next();
};
};
const configureQuery = (builder, view, configuration) => {
let build = builder;
for (const candidate of Array$1.reverse(configuration.interceptors ?? [])) {
if (!Predicate.isFunction(candidate)) throw new TypeError("Query interceptors must be functions");
const interceptor = candidate;
const previous = build;
build = (options) => {
const built = previous(options);
const { queryFn } = built;
if (queryFn === skipToken$1) return built;
return {
...built,
queryFn: async (context) => {
let original;
const result = await interceptor({
context,
next: once(async () => {
const data = await queryFn(context);
original = data;
return data;
})
});
return view === "metadata" ? preserveMetadataPolicy(original, result) : result;
}
};
};
}
const { policy } = configuration;
if (policy !== void 0 && Object.keys(policy).length > 0) {
const previous = build;
build = (options) => previous({
...policy,
...options
});
}
return build;
};
const configureMutation = (builder, configuration) => {
let build = builder;
for (const candidate of Array$1.reverse(configuration.interceptors ?? [])) {
if (!Predicate.isFunction(candidate)) throw new TypeError("Mutation interceptors must be functions");
const interceptor = candidate;
const previous = build;
build = (options) => {
const built = previous(options);
return {
...built,
mutationFn: async (input, context) => await interceptor({
input,
context,
next: once(async () => await built.mutationFn(input, context))
})
};
};
}
const { policy } = configuration;
if (policy !== void 0 && Object.keys(policy).length > 0) {
const previous = build;
build = configuration.callbacks === "compose" ? (options = {}) => {
const merged = {
...policy,
...options
};
for (const name of [
"onSuccess",
"onError",
"onSettled"
]) {
const before = policy[name];
const after = options[name];
if (Object.hasOwn(options, name) && after === void 0) continue;
if (isCallback(before)) merged[name] = async (...args) => {
await before(...args);
if (isCallback(after)) await after(...args);
};
}
return previous(merged);
} : (options) => previous({
...policy,
...options
});
}
return build;
};
//#endregion
//#region src/core/configure-utils.ts
const configuredLeaves = /* @__PURE__ */ new WeakMap();
const views = [
"query",
"infinite",
"metadata",
"live",
"streamed"
];
const snapshotConfiguration = (configuration) => {
const snapshot = {};
for (const view of [...views, "mutation"]) {
const entry = configuration[view];
if (entry !== void 0) snapshot[view] = Object.freeze({
...entry,
policy: entry.policy === void 0 ? void 0 : Object.freeze({ ...entry.policy }),
interceptors: entry.interceptors === void 0 ? void 0 : Object.freeze([...entry.interceptors])
});
}
return Object.freeze(snapshot);
};
const scopeConfigurations = (layers) => {
const scopes = /* @__PURE__ */ new Map();
for (const { depth, options } of layers) {
const scope = scopes.get(depth) ?? {};
for (const view of [...views, "mutation"]) {
const entry = options[view];
if (entry === void 0) continue;
const previous = scope[view];
scope[view] = {
policy: {
...previous?.policy,
...entry.policy
},
interceptors: [...previous?.interceptors ?? [], ...entry.interceptors ?? []],
callbacks: entry.callbacks ?? previous?.callbacks
};
}
scopes.set(depth, scope);
}
return [...scopes.values()];
};
const configureLeaf = (node, configurations) => {
const configured = { ...node };
for (const view of views) {
if (!Object.hasOwn(node, `${view}Options`)) continue;
const inherited = configurations.flatMap((entry) => entry[view] === void 0 ? [] : [entry[view]]);
if (inherited.length === 0) continue;
const policy = {};
for (const entry of inherited) Object.assign(policy, entry.policy);
const name = `${view}Options`;
const builder = node[name];
if (!Predicate.isFunction(builder)) throw new TypeError(`${name} must be a function`);
configured[name] = configureQuery(builder, view, {
policy,
interceptors: inherited.flatMap((entry) => entry.interceptors ?? [])
});
if (view === "streamed") {
const { streamedKey } = node;
if (!Predicate.isFunction(streamedKey)) throw new TypeError("Streamed queries require a key builder");
const buildKey = streamedKey;
const history = Object.fromEntries(Object.entries(policy).filter(([option]) => [
"maxChunks",
"refetchMode",
"publicationBatchSize",
"publicationMaxWaitMs"
].includes(option)));
configured["streamedKey"] = inputKind(node.key) === "Inputless" ? (options) => buildKey({
...history,
...options
}) : (input, options) => buildKey(input, {
...history,
...options
});
}
}
if (Object.hasOwn(node, "mutationOptions")) {
const mutations = configurations.flatMap((entry) => entry.mutation === void 0 ? [] : [entry.mutation]);
if (mutations.length > 0) {
const candidate = node["mutationOptions"];
if (!Predicate.isFunction(candidate)) throw new TypeError("mutationOptions must be a function");
let builder = candidate;
for (const entry of mutations) builder = configureMutation(builder, {
policy: entry.policy,
callbacks: entry.callbacks
});
const interceptors = mutations.flatMap((entry) => entry.interceptors ?? []);
if (interceptors.length > 0) builder = configureMutation(builder, { interceptors });
configured["mutationOptions"] = builder;
}
}
return configured;
};
const configureNode = (node, inherited, configuration) => {
const configurations = [...inherited, {
depth: node.key().length,
options: snapshotConfiguration(configuration)
}];
const children = Object.entries(node).filter((entry) => Predicate.hasProperty(entry[1], "key") && Predicate.isFunction(entry[1].key));
for (const name of Object.keys(configuration.scoped ?? {})) if (!children.some(([key]) => key === name)) throw new TypeError(`Unknown utility scope: ${name}`);
if (children.length === 0) {
const previous = configuredLeaves.get(node);
const original = previous?.original ?? node;
const layers = Array$1.sortWith([...previous?.layers ?? [], ...configurations], (layer) => layer.depth, Order.Number);
const configured = Object.freeze({ ...configureLeaf(original, scopeConfigurations(layers)) });
configuredLeaves.set(configured, {
original,
layers
});
return configured;
}
return Object.freeze({
...node,
...Object.fromEntries(children.map(([key, child]) => [key, configureNode(child, configurations, configuration.scoped !== void 0 && Object.hasOwn(configuration.scoped, key) ? configuration.scoped[key] ?? {} : {})]))
});
};
/** Applies root, branch, and leaf configuration while preserving generated cache identity. */
const configureUtils = (tree, configuration) => {
return configureNode(tree, [], configuration);
};
//#endregion
//#region src/core/branch-filter.ts
const isBranchNode = (node) => Predicate.hasProperty(node, "key") && Predicate.isFunction(node.key);
/** Selects one view across a generated root, branch or leaf without changing cache keys. */
const createBranchQueryFilter = (branch, view) => {
const byLength = /* @__PURE__ */ new Map();
const seen = /* @__PURE__ */ new WeakSet();
const visit = (node) => {
if (seen.has(node)) return;
seen.add(node);
if (Predicate.hasProperty(node, `${view}Options`)) {
const prefix = [...node.key(), view];
const entries = byLength.get(prefix.length) ?? /* @__PURE__ */ new Set();
entries.add(hashKey(prefix));
byLength.set(prefix.length, entries);
}
for (const child of Object.values(node)) if (isBranchNode(child)) visit(child);
};
visit(branch);
const queryKey = branch.key();
return (options = {}) => ({
...options,
queryKey,
predicate: (query) => {
for (const [length, prefixes] of byLength) if (query.queryKey.length >= length && prefixes.has(hashKey(query.queryKey.slice(0, length)))) return options.predicate?.(query) ?? true;
return false;
}
});
};
//#endregion
//#region src/core/declared-error.ts
/** Matches one typed failure only; defects, interruptions and composite Causes never match. */
const findDeclaredError = (error, tag) => {
if (!isEffectRpcQueryError(error) && !isEffectHttpApiQueryError(error)) return Option.none();
const [reason] = error.cause.reasons;
if (error.cause.reasons.length !== 1 || reason?._tag !== "Fail") return Option.none();
const failure = reason.error;
if (!Predicate.hasProperty(failure, "_tag") || failure._tag !== tag) return Option.none();
const matched = failure;
return Option.some(matched);
};
//#endregion
//#region src/core/consume-events.ts
/** Consumes events sequentially without retaining history or owning a runtime or cache. */
const consumeEvents = (source, options) => Effect.suspend(() => (options.onStart ?? Effect.void).pipe(Effect.andThen(Stream.runForEach(source, options.onEvent)), Effect.onExit((exit) => options.onExit?.(exit) ?? Effect.void)));
//#endregion
export { EffectHttpApiQueryConfigError, EffectHttpApiQueryEmptyStreamError, EffectHttpApiQueryError, EffectHttpApiQueryKeyError, EffectRpcQueryConfigError, EffectRpcQueryEmptyStreamError, EffectRpcQueryError, EffectRpcQueryKeyError, HydrationCodecError, configureUtils, consumeEvents, createBranchQueryFilter, createHttpApiQueryUtils, createHydrationRegistry, createQueryFilter, createRpcQueryUtils, fetchStreamSnapshot, findDeclaredError, hydrationCallbacks, infiniteQueryCodec, isEffectHttpApiQueryError, isEffectRpcQueryError, prepareInput, queryViewCodec, recoverStream, skipToken, viewPrefix };
//# sourceMappingURL=index.mjs.map