@vielzeug/sourcerer 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/dist/_dev.d.ts +2 -0
- package/dist/_dev.d.ts.map +1 -0
- package/dist/_utils.cjs +2 -0
- package/dist/_utils.cjs.map +1 -0
- package/dist/_utils.d.ts +8 -0
- package/dist/_utils.d.ts.map +1 -0
- package/dist/_utils.js +7 -0
- package/dist/_utils.js.map +1 -0
- package/dist/applyQuery.cjs +2 -0
- package/dist/applyQuery.cjs.map +1 -0
- package/dist/applyQuery.d.ts +20 -0
- package/dist/applyQuery.d.ts.map +1 -0
- package/dist/applyQuery.js +8 -0
- package/dist/applyQuery.js.map +1 -0
- package/dist/asyncSource.cjs +2 -0
- package/dist/asyncSource.cjs.map +1 -0
- package/dist/asyncSource.d.ts +62 -0
- package/dist/asyncSource.d.ts.map +1 -0
- package/dist/asyncSource.js +40 -0
- package/dist/asyncSource.js.map +1 -0
- package/dist/codecs.cjs +2 -0
- package/dist/codecs.cjs.map +1 -0
- package/dist/codecs.d.ts +30 -0
- package/dist/codecs.d.ts.map +1 -0
- package/dist/codecs.js +36 -0
- package/dist/codecs.js.map +1 -0
- package/dist/core.cjs +2 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.ts +47 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +59 -0
- package/dist/core.js.map +1 -0
- package/dist/cursorSource.cjs +2 -0
- package/dist/cursorSource.cjs.map +1 -0
- package/dist/cursorSource.d.ts +4 -0
- package/dist/cursorSource.d.ts.map +1 -0
- package/dist/cursorSource.js +135 -0
- package/dist/cursorSource.js.map +1 -0
- package/dist/derive.cjs +2 -0
- package/dist/derive.cjs.map +1 -0
- package/dist/derive.d.ts +28 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +40 -0
- package/dist/derive.js.map +1 -0
- package/dist/errors.cjs +2 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.ts +62 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -0
- package/dist/fetchManager.cjs +2 -0
- package/dist/fetchManager.cjs.map +1 -0
- package/dist/fetchManager.d.ts +27 -0
- package/dist/fetchManager.d.ts.map +1 -0
- package/dist/fetchManager.js +41 -0
- package/dist/fetchManager.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/infiniteSource.cjs +2 -0
- package/dist/infiniteSource.cjs.map +1 -0
- package/dist/infiniteSource.d.ts +4 -0
- package/dist/infiniteSource.d.ts.map +1 -0
- package/dist/infiniteSource.js +135 -0
- package/dist/infiniteSource.js.map +1 -0
- package/dist/localSource.cjs +2 -0
- package/dist/localSource.cjs.map +1 -0
- package/dist/localSource.d.ts +24 -0
- package/dist/localSource.d.ts.map +1 -0
- package/dist/localSource.js +130 -0
- package/dist/localSource.js.map +1 -0
- package/dist/merge.cjs +2 -0
- package/dist/merge.cjs.map +1 -0
- package/dist/merge.d.ts +30 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +34 -0
- package/dist/merge.js.map +1 -0
- package/dist/pagination.cjs +2 -0
- package/dist/pagination.cjs.map +1 -0
- package/dist/pagination.d.ts +31 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +34 -0
- package/dist/pagination.js.map +1 -0
- package/dist/prefetch.cjs +2 -0
- package/dist/prefetch.cjs.map +1 -0
- package/dist/prefetch.d.ts +32 -0
- package/dist/prefetch.d.ts.map +1 -0
- package/dist/prefetch.js +38 -0
- package/dist/prefetch.js.map +1 -0
- package/dist/presets.cjs +2 -0
- package/dist/presets.cjs.map +1 -0
- package/dist/presets.d.ts +12 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +24 -0
- package/dist/presets.js.map +1 -0
- package/dist/remoteSource.cjs +2 -0
- package/dist/remoteSource.cjs.map +1 -0
- package/dist/remoteSource.d.ts +4 -0
- package/dist/remoteSource.d.ts.map +1 -0
- package/dist/remoteSource.js +175 -0
- package/dist/remoteSource.js.map +1 -0
- package/dist/sourcerer.cjs +2 -0
- package/dist/sourcerer.cjs.map +1 -0
- package/dist/sourcerer.iife.js +2 -0
- package/dist/sourcerer.iife.js.map +1 -0
- package/dist/sourcerer.js +2 -0
- package/dist/sourcerer.js.map +1 -0
- package/dist/state.cjs +2 -0
- package/dist/state.cjs.map +1 -0
- package/dist/state.d.ts +23 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +14 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +374 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursorSource.cjs","names":[],"sources":["../src/cursorSource.ts"],"sourcesContent":["import type { CursorConfig, CursorMeta, CursorSource, CursorSourceQuery, SearchOptions } from './types';\n\nimport { defaultKeyOf, extractError, retry } from './_utils';\nimport { createAsyncSource } from './asyncSource';\nimport { SourcererError } from './errors';\n\ntype PendingSearch = { promise: Promise<void>; resolve: () => void };\n\n/** Creates a cursor-based (keyset-pagination) source that fetches data from a network endpoint. */\nexport function createCursorSource<T, TCursor = string>(cfg: CursorConfig<T, TCursor>): CursorSource<T, TCursor> {\n const keyOf = cfg.queryKey ?? defaultKeyOf;\n\n // ── Mutable state ───────────────────────────────────────────────────────────\n let limit = Math.max(1, Math.trunc(cfg.limit ?? 20));\n let search = '';\n let afterCursor: TCursor | undefined;\n let beforeCursor: TCursor | undefined;\n let nextCursor: TCursor | undefined;\n let prevCursor: TCursor | undefined;\n let items: readonly T[] = [];\n let total = 0;\n let error: SourcererError | null = null;\n let pendingSearch: PendingSearch | null = null;\n\n // ── Cached accessors ────────────────────────────────────────────────────────\n let cachedCurrent: readonly T[] = [];\n let cachedMeta: CursorMeta = {\n error: null,\n hasNextPage: false,\n hasPrevPage: false,\n isLoading: false,\n isSearchPending: false,\n pageSize: limit,\n totalItems: 0,\n };\n\n const refreshMeta = () => {\n cachedCurrent = items;\n cachedMeta = {\n error,\n hasNextPage: nextCursor !== undefined,\n hasPrevPage: prevCursor !== undefined,\n isLoading: base.pendingCount() > 0,\n isSearchPending: base.core.isScheduled,\n pageSize: limit,\n totalItems: total,\n };\n };\n\n // onBeforeNotify ensures refreshMeta() runs before any subscriber observes the new state,\n // eliminating the need for a parallel listeners Set.\n const base = createAsyncSource<CursorSourceQuery<TCursor>>(cfg, keyOf, refreshMeta);\n const { autoFetch, debounceMs, retryAttempts, retryDelay } = base;\n\n refreshMeta();\n\n const notifyListeners = () => {\n if (base.disposed) return;\n\n base.core.notify();\n };\n\n const resolvePendingSearch = () => {\n if (pendingSearch) {\n pendingSearch.resolve();\n pendingSearch = null;\n }\n };\n\n // ── Core fetch ──────────────────────────────────────────────────────────────\n const toRemoteQuery = (): CursorSourceQuery<TCursor> => ({\n ...(afterCursor !== undefined && { after: afterCursor }),\n ...(beforeCursor !== undefined && { before: beforeCursor }),\n ...(search && { search }),\n limit,\n });\n\n const fetchQuery = (q: CursorSourceQuery<TCursor>): Promise<void> => {\n error = null;\n\n return base.fetch(\n q,\n async (q, signal, isLatest) => {\n const startTime = Date.now();\n\n try {\n const result = await retry((sig) => cfg.fetch(q, sig!), {\n delay: retryDelay,\n signal,\n times: retryAttempts + 1,\n });\n\n if (isLatest()) {\n items = result.items;\n total = result.total ?? result.items.length;\n nextCursor = result.nextCursor;\n prevCursor = result.prevCursor;\n error = null;\n cfg.onFetch?.({ durationMs: Date.now() - startTime, query: q, status: 'success' });\n }\n } catch (reason: unknown) {\n if (signal.aborted) return;\n\n if (isLatest()) {\n items = [];\n total = 0;\n nextCursor = undefined;\n prevCursor = undefined;\n error = new SourcererError(extractError(reason), {\n cause: reason,\n context: { kind: 'cursor', limit: q.limit, ...(q.search && { search: q.search }) },\n });\n cfg.onFetch?.({ durationMs: Date.now() - startTime, error, query: q, status: 'error' });\n }\n }\n },\n () => {\n notifyListeners();\n\n if (base.pendingCount() === 0) resolvePendingSearch();\n },\n );\n };\n\n const doUpdate = () => fetchQuery(toRemoteQuery());\n\n // ── Auto-fetch + refresh interval ───────────────────────────────────────────\n if (autoFetch) void doUpdate();\n\n base.startRefreshInterval(() => void doUpdate());\n\n // ── Public API ──────────────────────────────────────────────────────────────\n return {\n get current() {\n return cachedCurrent;\n },\n\n get disposalSignal() {\n return base.disposalSignal;\n },\n\n dispose: () => {\n resolvePendingSearch();\n base.dispose();\n },\n\n get disposed() {\n return base.disposed;\n },\n\n get meta() {\n return cachedMeta;\n },\n\n next() {\n if (!nextCursor) return Promise.resolve();\n\n afterCursor = nextCursor;\n beforeCursor = undefined;\n\n return doUpdate();\n },\n\n patch(changes) {\n let changed = false;\n\n if (changes.limit !== undefined) {\n const next = Math.max(1, Math.trunc(changes.limit));\n\n if (next !== limit) {\n limit = next;\n afterCursor = undefined;\n beforeCursor = undefined;\n changed = true;\n }\n }\n\n // `|| base.core.isScheduled` also catches patching in the same search text as an already-pending\n // debounced search() call — that still needs flushing, not silently dropping as a no-op.\n if ('search' in changes && (changes.search !== search || base.core.isScheduled)) {\n search = changes.search ?? '';\n afterCursor = undefined;\n beforeCursor = undefined;\n changed = true;\n }\n\n if (!changed) return Promise.resolve();\n\n // patch() is documented as a single atomic fetch — cancel any debounced search()\n // still pending so it doesn't fire a second, redundant fetch afterwards.\n base.core.cancelTimer();\n resolvePendingSearch();\n\n return doUpdate();\n },\n\n prev() {\n if (!prevCursor) return Promise.resolve();\n\n beforeCursor = prevCursor;\n afterCursor = undefined;\n\n return doUpdate();\n },\n\n get query() {\n return {\n ...(afterCursor !== undefined && { after: afterCursor }),\n ...(beforeCursor !== undefined && { before: beforeCursor }),\n ...(search && { search }),\n limit,\n };\n },\n\n ready(timeout) {\n return base.ready(timeout);\n },\n\n refresh() {\n return doUpdate();\n },\n\n reset() {\n base.core.cancelTimer();\n resolvePendingSearch();\n search = '';\n afterCursor = undefined;\n beforeCursor = undefined;\n\n return doUpdate();\n },\n\n search(q, opts?: SearchOptions): Promise<void> {\n if (opts?.immediate) {\n // A pending debounced search for this same text still needs flushing —\n // `q === search` alone doesn't mean the fetch already happened.\n if (q === search && !base.core.isScheduled) return Promise.resolve();\n\n base.core.cancelTimer();\n resolvePendingSearch();\n search = q;\n afterCursor = undefined;\n beforeCursor = undefined;\n\n return doUpdate();\n }\n\n if (q === search) return Promise.resolve();\n\n resolvePendingSearch();\n\n search = q;\n afterCursor = undefined;\n beforeCursor = undefined;\n\n let resolveSearch!: () => void;\n const promise = new Promise<void>((res) => {\n resolveSearch = res;\n });\n\n pendingSearch = { promise, resolve: resolveSearch };\n\n base.core.schedule(() => void doUpdate(), debounceMs);\n notifyListeners();\n\n return promise;\n },\n\n subscribe: (listener) => base.core.subscribe(listener),\n\n [Symbol.dispose]: () => {\n resolvePendingSearch();\n base.dispose();\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CursorConfig, CursorSource } from './types';
|
|
2
|
+
/** Creates a cursor-based (keyset-pagination) source that fetches data from a network endpoint. */
|
|
3
|
+
export declare function createCursorSource<T, TCursor = string>(cfg: CursorConfig<T, TCursor>): CursorSource<T, TCursor>;
|
|
4
|
+
//# sourceMappingURL=cursorSource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursorSource.d.ts","sourceRoot":"","sources":["../src/cursorSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAc,YAAY,EAAoC,MAAM,SAAS,CAAC;AAQxG,mGAAmG;AACnG,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CA0Q/G"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { SourcererError as e } from "./errors.js";
|
|
2
|
+
import { defaultKeyOf as t, extractError as n, retry as r } from "./_utils.js";
|
|
3
|
+
import { createAsyncSource as i } from "./asyncSource.js";
|
|
4
|
+
//#region src/cursorSource.ts
|
|
5
|
+
function a(a) {
|
|
6
|
+
let o = a.queryKey ?? t, s = Math.max(1, Math.trunc(a.limit ?? 20)), c = "", l, u, d, f, p = [], m = 0, h = null, g = null, _ = [], v = {
|
|
7
|
+
error: null,
|
|
8
|
+
hasNextPage: !1,
|
|
9
|
+
hasPrevPage: !1,
|
|
10
|
+
isLoading: !1,
|
|
11
|
+
isSearchPending: !1,
|
|
12
|
+
pageSize: s,
|
|
13
|
+
totalItems: 0
|
|
14
|
+
}, y = () => {
|
|
15
|
+
_ = p, v = {
|
|
16
|
+
error: h,
|
|
17
|
+
hasNextPage: d !== void 0,
|
|
18
|
+
hasPrevPage: f !== void 0,
|
|
19
|
+
isLoading: b.pendingCount() > 0,
|
|
20
|
+
isSearchPending: b.core.isScheduled,
|
|
21
|
+
pageSize: s,
|
|
22
|
+
totalItems: m
|
|
23
|
+
};
|
|
24
|
+
}, b = i(a, o, y), { autoFetch: x, debounceMs: S, retryAttempts: C, retryDelay: w } = b;
|
|
25
|
+
y();
|
|
26
|
+
let T = () => {
|
|
27
|
+
b.disposed || b.core.notify();
|
|
28
|
+
}, E = () => {
|
|
29
|
+
g &&= (g.resolve(), null);
|
|
30
|
+
}, D = () => ({
|
|
31
|
+
...l !== void 0 && { after: l },
|
|
32
|
+
...u !== void 0 && { before: u },
|
|
33
|
+
...c && { search: c },
|
|
34
|
+
limit: s
|
|
35
|
+
}), O = (t) => (h = null, b.fetch(t, async (t, i, o) => {
|
|
36
|
+
let s = Date.now();
|
|
37
|
+
try {
|
|
38
|
+
let e = await r((e) => a.fetch(t, e), {
|
|
39
|
+
delay: w,
|
|
40
|
+
signal: i,
|
|
41
|
+
times: C + 1
|
|
42
|
+
});
|
|
43
|
+
o() && (p = e.items, m = e.total ?? e.items.length, d = e.nextCursor, f = e.prevCursor, h = null, a.onFetch?.({
|
|
44
|
+
durationMs: Date.now() - s,
|
|
45
|
+
query: t,
|
|
46
|
+
status: "success"
|
|
47
|
+
}));
|
|
48
|
+
} catch (r) {
|
|
49
|
+
if (i.aborted) return;
|
|
50
|
+
o() && (p = [], m = 0, d = void 0, f = void 0, h = new e(n(r), {
|
|
51
|
+
cause: r,
|
|
52
|
+
context: {
|
|
53
|
+
kind: "cursor",
|
|
54
|
+
limit: t.limit,
|
|
55
|
+
...t.search && { search: t.search }
|
|
56
|
+
}
|
|
57
|
+
}), a.onFetch?.({
|
|
58
|
+
durationMs: Date.now() - s,
|
|
59
|
+
error: h,
|
|
60
|
+
query: t,
|
|
61
|
+
status: "error"
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
}, () => {
|
|
65
|
+
T(), b.pendingCount() === 0 && E();
|
|
66
|
+
})), k = () => O(D());
|
|
67
|
+
return x && k(), b.startRefreshInterval(() => void k()), {
|
|
68
|
+
get current() {
|
|
69
|
+
return _;
|
|
70
|
+
},
|
|
71
|
+
get disposalSignal() {
|
|
72
|
+
return b.disposalSignal;
|
|
73
|
+
},
|
|
74
|
+
dispose: () => {
|
|
75
|
+
E(), b.dispose();
|
|
76
|
+
},
|
|
77
|
+
get disposed() {
|
|
78
|
+
return b.disposed;
|
|
79
|
+
},
|
|
80
|
+
get meta() {
|
|
81
|
+
return v;
|
|
82
|
+
},
|
|
83
|
+
next() {
|
|
84
|
+
return d ? (l = d, u = void 0, k()) : Promise.resolve();
|
|
85
|
+
},
|
|
86
|
+
patch(e) {
|
|
87
|
+
let t = !1;
|
|
88
|
+
if (e.limit !== void 0) {
|
|
89
|
+
let n = Math.max(1, Math.trunc(e.limit));
|
|
90
|
+
n !== s && (s = n, l = void 0, u = void 0, t = !0);
|
|
91
|
+
}
|
|
92
|
+
return "search" in e && (e.search !== c || b.core.isScheduled) && (c = e.search ?? "", l = void 0, u = void 0, t = !0), t ? (b.core.cancelTimer(), E(), k()) : Promise.resolve();
|
|
93
|
+
},
|
|
94
|
+
prev() {
|
|
95
|
+
return f ? (u = f, l = void 0, k()) : Promise.resolve();
|
|
96
|
+
},
|
|
97
|
+
get query() {
|
|
98
|
+
return {
|
|
99
|
+
...l !== void 0 && { after: l },
|
|
100
|
+
...u !== void 0 && { before: u },
|
|
101
|
+
...c && { search: c },
|
|
102
|
+
limit: s
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
ready(e) {
|
|
106
|
+
return b.ready(e);
|
|
107
|
+
},
|
|
108
|
+
refresh() {
|
|
109
|
+
return k();
|
|
110
|
+
},
|
|
111
|
+
reset() {
|
|
112
|
+
return b.core.cancelTimer(), E(), c = "", l = void 0, u = void 0, k();
|
|
113
|
+
},
|
|
114
|
+
search(e, t) {
|
|
115
|
+
if (t?.immediate) return e === c && !b.core.isScheduled ? Promise.resolve() : (b.core.cancelTimer(), E(), c = e, l = void 0, u = void 0, k());
|
|
116
|
+
if (e === c) return Promise.resolve();
|
|
117
|
+
E(), c = e, l = void 0, u = void 0;
|
|
118
|
+
let n, r = new Promise((e) => {
|
|
119
|
+
n = e;
|
|
120
|
+
});
|
|
121
|
+
return g = {
|
|
122
|
+
promise: r,
|
|
123
|
+
resolve: n
|
|
124
|
+
}, b.core.schedule(() => void k(), S), T(), r;
|
|
125
|
+
},
|
|
126
|
+
subscribe: (e) => b.core.subscribe(e),
|
|
127
|
+
[Symbol.dispose]: () => {
|
|
128
|
+
E(), b.dispose();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
export { a as createCursorSource };
|
|
134
|
+
|
|
135
|
+
//# sourceMappingURL=cursorSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursorSource.js","names":[],"sources":["../src/cursorSource.ts"],"sourcesContent":["import type { CursorConfig, CursorMeta, CursorSource, CursorSourceQuery, SearchOptions } from './types';\n\nimport { defaultKeyOf, extractError, retry } from './_utils';\nimport { createAsyncSource } from './asyncSource';\nimport { SourcererError } from './errors';\n\ntype PendingSearch = { promise: Promise<void>; resolve: () => void };\n\n/** Creates a cursor-based (keyset-pagination) source that fetches data from a network endpoint. */\nexport function createCursorSource<T, TCursor = string>(cfg: CursorConfig<T, TCursor>): CursorSource<T, TCursor> {\n const keyOf = cfg.queryKey ?? defaultKeyOf;\n\n // ── Mutable state ───────────────────────────────────────────────────────────\n let limit = Math.max(1, Math.trunc(cfg.limit ?? 20));\n let search = '';\n let afterCursor: TCursor | undefined;\n let beforeCursor: TCursor | undefined;\n let nextCursor: TCursor | undefined;\n let prevCursor: TCursor | undefined;\n let items: readonly T[] = [];\n let total = 0;\n let error: SourcererError | null = null;\n let pendingSearch: PendingSearch | null = null;\n\n // ── Cached accessors ────────────────────────────────────────────────────────\n let cachedCurrent: readonly T[] = [];\n let cachedMeta: CursorMeta = {\n error: null,\n hasNextPage: false,\n hasPrevPage: false,\n isLoading: false,\n isSearchPending: false,\n pageSize: limit,\n totalItems: 0,\n };\n\n const refreshMeta = () => {\n cachedCurrent = items;\n cachedMeta = {\n error,\n hasNextPage: nextCursor !== undefined,\n hasPrevPage: prevCursor !== undefined,\n isLoading: base.pendingCount() > 0,\n isSearchPending: base.core.isScheduled,\n pageSize: limit,\n totalItems: total,\n };\n };\n\n // onBeforeNotify ensures refreshMeta() runs before any subscriber observes the new state,\n // eliminating the need for a parallel listeners Set.\n const base = createAsyncSource<CursorSourceQuery<TCursor>>(cfg, keyOf, refreshMeta);\n const { autoFetch, debounceMs, retryAttempts, retryDelay } = base;\n\n refreshMeta();\n\n const notifyListeners = () => {\n if (base.disposed) return;\n\n base.core.notify();\n };\n\n const resolvePendingSearch = () => {\n if (pendingSearch) {\n pendingSearch.resolve();\n pendingSearch = null;\n }\n };\n\n // ── Core fetch ──────────────────────────────────────────────────────────────\n const toRemoteQuery = (): CursorSourceQuery<TCursor> => ({\n ...(afterCursor !== undefined && { after: afterCursor }),\n ...(beforeCursor !== undefined && { before: beforeCursor }),\n ...(search && { search }),\n limit,\n });\n\n const fetchQuery = (q: CursorSourceQuery<TCursor>): Promise<void> => {\n error = null;\n\n return base.fetch(\n q,\n async (q, signal, isLatest) => {\n const startTime = Date.now();\n\n try {\n const result = await retry((sig) => cfg.fetch(q, sig!), {\n delay: retryDelay,\n signal,\n times: retryAttempts + 1,\n });\n\n if (isLatest()) {\n items = result.items;\n total = result.total ?? result.items.length;\n nextCursor = result.nextCursor;\n prevCursor = result.prevCursor;\n error = null;\n cfg.onFetch?.({ durationMs: Date.now() - startTime, query: q, status: 'success' });\n }\n } catch (reason: unknown) {\n if (signal.aborted) return;\n\n if (isLatest()) {\n items = [];\n total = 0;\n nextCursor = undefined;\n prevCursor = undefined;\n error = new SourcererError(extractError(reason), {\n cause: reason,\n context: { kind: 'cursor', limit: q.limit, ...(q.search && { search: q.search }) },\n });\n cfg.onFetch?.({ durationMs: Date.now() - startTime, error, query: q, status: 'error' });\n }\n }\n },\n () => {\n notifyListeners();\n\n if (base.pendingCount() === 0) resolvePendingSearch();\n },\n );\n };\n\n const doUpdate = () => fetchQuery(toRemoteQuery());\n\n // ── Auto-fetch + refresh interval ───────────────────────────────────────────\n if (autoFetch) void doUpdate();\n\n base.startRefreshInterval(() => void doUpdate());\n\n // ── Public API ──────────────────────────────────────────────────────────────\n return {\n get current() {\n return cachedCurrent;\n },\n\n get disposalSignal() {\n return base.disposalSignal;\n },\n\n dispose: () => {\n resolvePendingSearch();\n base.dispose();\n },\n\n get disposed() {\n return base.disposed;\n },\n\n get meta() {\n return cachedMeta;\n },\n\n next() {\n if (!nextCursor) return Promise.resolve();\n\n afterCursor = nextCursor;\n beforeCursor = undefined;\n\n return doUpdate();\n },\n\n patch(changes) {\n let changed = false;\n\n if (changes.limit !== undefined) {\n const next = Math.max(1, Math.trunc(changes.limit));\n\n if (next !== limit) {\n limit = next;\n afterCursor = undefined;\n beforeCursor = undefined;\n changed = true;\n }\n }\n\n // `|| base.core.isScheduled` also catches patching in the same search text as an already-pending\n // debounced search() call — that still needs flushing, not silently dropping as a no-op.\n if ('search' in changes && (changes.search !== search || base.core.isScheduled)) {\n search = changes.search ?? '';\n afterCursor = undefined;\n beforeCursor = undefined;\n changed = true;\n }\n\n if (!changed) return Promise.resolve();\n\n // patch() is documented as a single atomic fetch — cancel any debounced search()\n // still pending so it doesn't fire a second, redundant fetch afterwards.\n base.core.cancelTimer();\n resolvePendingSearch();\n\n return doUpdate();\n },\n\n prev() {\n if (!prevCursor) return Promise.resolve();\n\n beforeCursor = prevCursor;\n afterCursor = undefined;\n\n return doUpdate();\n },\n\n get query() {\n return {\n ...(afterCursor !== undefined && { after: afterCursor }),\n ...(beforeCursor !== undefined && { before: beforeCursor }),\n ...(search && { search }),\n limit,\n };\n },\n\n ready(timeout) {\n return base.ready(timeout);\n },\n\n refresh() {\n return doUpdate();\n },\n\n reset() {\n base.core.cancelTimer();\n resolvePendingSearch();\n search = '';\n afterCursor = undefined;\n beforeCursor = undefined;\n\n return doUpdate();\n },\n\n search(q, opts?: SearchOptions): Promise<void> {\n if (opts?.immediate) {\n // A pending debounced search for this same text still needs flushing —\n // `q === search` alone doesn't mean the fetch already happened.\n if (q === search && !base.core.isScheduled) return Promise.resolve();\n\n base.core.cancelTimer();\n resolvePendingSearch();\n search = q;\n afterCursor = undefined;\n beforeCursor = undefined;\n\n return doUpdate();\n }\n\n if (q === search) return Promise.resolve();\n\n resolvePendingSearch();\n\n search = q;\n afterCursor = undefined;\n beforeCursor = undefined;\n\n let resolveSearch!: () => void;\n const promise = new Promise<void>((res) => {\n resolveSearch = res;\n });\n\n pendingSearch = { promise, resolve: resolveSearch };\n\n base.core.schedule(() => void doUpdate(), debounceMs);\n notifyListeners();\n\n return promise;\n },\n\n subscribe: (listener) => base.core.subscribe(listener),\n\n [Symbol.dispose]: () => {\n resolvePendingSearch();\n base.dispose();\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/derive.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./core.cjs");function t(t,n){let r=e.createSourceCore(),i=[],a=()=>{i=n(t.current)};a();let o=t.subscribe(()=>{a(),r.notify()});`disposalSignal`in t&&t.disposalSignal instanceof AbortSignal&&t.disposalSignal.addEventListener(`abort`,()=>r.dispose(),{once:!0});let s={get current(){return i},get disposalSignal(){return r.disposalSignal},dispose(){r.isDisposed||(o(),r.dispose())},get disposed(){return r.isDisposed},get meta(){return t.meta},subscribe(e){return r.subscribe(e)},[Symbol.dispose](){s.dispose()}};return s}exports.deriveSource=t;
|
|
2
|
+
//# sourceMappingURL=derive.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive.cjs","names":[],"sources":["../src/derive.ts"],"sourcesContent":["import type { DerivedSource } from './types';\n\nimport { createSourceCore } from './core';\n\n/**\n * Creates a reactive read-only projection derived from a parent source.\n *\n * The derived source mirrors the parent's `meta` and applies `transform` to `current`\n * whenever the parent changes. This is useful for client-side projections (e.g. additional\n * filtering, mapping, or sorting) over a remote source without re-fetching.\n *\n * Call `dispose()` to stop listening to the parent.\n *\n * @example\n * ```ts\n * const source = createRemoteSource<User>({ fetch: fetchUsers, limit: 20 });\n *\n * // Show only active users without re-fetching.\n * const activeUsers = deriveSource(source, (items) => items.filter((u) => u.active));\n *\n * activeUsers.subscribe(() => {\n * console.log(activeUsers.current, activeUsers.meta.totalItems);\n * });\n * ```\n */\nexport function deriveSource<T, U, TMeta>(\n parent: {\n readonly current: readonly T[];\n readonly meta: TMeta;\n subscribe(listener: () => void): () => void;\n },\n transform: (items: readonly T[]) => readonly U[],\n): DerivedSource<U, TMeta> {\n const core = createSourceCore();\n let cachedCurrent: readonly U[] = [];\n\n const applyTransform = () => {\n cachedCurrent = transform(parent.current);\n };\n\n applyTransform();\n\n const unsubscribeParent = parent.subscribe(() => {\n applyTransform();\n core.notify();\n });\n\n if ('disposalSignal' in parent && parent.disposalSignal instanceof AbortSignal) {\n parent.disposalSignal.addEventListener('abort', () => core.dispose(), { once: true });\n }\n\n const source: DerivedSource<U, TMeta> = {\n get current() {\n return cachedCurrent;\n },\n\n get disposalSignal() {\n return core.disposalSignal;\n },\n\n dispose() {\n if (core.isDisposed) return;\n\n unsubscribeParent();\n core.dispose();\n },\n\n get disposed() {\n return core.isDisposed;\n },\n\n get meta() {\n return parent.meta;\n },\n\n subscribe(listener) {\n return core.subscribe(listener);\n },\n\n [Symbol.dispose]() {\n source.dispose();\n },\n };\n\n return source;\n}\n"],"mappings":"8BAyBA,SAAgB,EACd,EAKA,EACyB,CACzB,IAAM,EAAO,EAAA,iBAAiB,EAC1B,EAA8B,CAAC,EAE7B,MAAuB,CAC3B,EAAgB,EAAU,EAAO,OAAO,CAC1C,EAEA,EAAe,EAEf,IAAM,EAAoB,EAAO,cAAgB,CAC/C,EAAe,EACf,EAAK,OAAO,CACd,CAAC,EAEG,mBAAoB,GAAU,EAAO,0BAA0B,aACjE,EAAO,eAAe,iBAAiB,YAAe,EAAK,QAAQ,EAAG,CAAE,KAAM,EAAK,CAAC,EAGtF,IAAM,EAAkC,CACtC,IAAI,SAAU,CACZ,OAAO,CACT,EAEA,IAAI,gBAAiB,CACnB,OAAO,EAAK,cACd,EAEA,SAAU,CACJ,EAAK,aAET,EAAkB,EAClB,EAAK,QAAQ,EACf,EAEA,IAAI,UAAW,CACb,OAAO,EAAK,UACd,EAEA,IAAI,MAAO,CACT,OAAO,EAAO,IAChB,EAEA,UAAU,EAAU,CAClB,OAAO,EAAK,UAAU,CAAQ,CAChC,EAEA,CAAC,OAAO,UAAW,CACjB,EAAO,QAAQ,CACjB,CACF,EAEA,OAAO,CACT"}
|
package/dist/derive.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DerivedSource } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a reactive read-only projection derived from a parent source.
|
|
4
|
+
*
|
|
5
|
+
* The derived source mirrors the parent's `meta` and applies `transform` to `current`
|
|
6
|
+
* whenever the parent changes. This is useful for client-side projections (e.g. additional
|
|
7
|
+
* filtering, mapping, or sorting) over a remote source without re-fetching.
|
|
8
|
+
*
|
|
9
|
+
* Call `dispose()` to stop listening to the parent.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const source = createRemoteSource<User>({ fetch: fetchUsers, limit: 20 });
|
|
14
|
+
*
|
|
15
|
+
* // Show only active users without re-fetching.
|
|
16
|
+
* const activeUsers = deriveSource(source, (items) => items.filter((u) => u.active));
|
|
17
|
+
*
|
|
18
|
+
* activeUsers.subscribe(() => {
|
|
19
|
+
* console.log(activeUsers.current, activeUsers.meta.totalItems);
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function deriveSource<T, U, TMeta>(parent: {
|
|
24
|
+
readonly current: readonly T[];
|
|
25
|
+
readonly meta: TMeta;
|
|
26
|
+
subscribe(listener: () => void): () => void;
|
|
27
|
+
}, transform: (items: readonly T[]) => readonly U[]): DerivedSource<U, TMeta>;
|
|
28
|
+
//# sourceMappingURL=derive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive.d.ts","sourceRoot":"","sources":["../src/derive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAI7C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EACtC,MAAM,EAAE;IACN,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC7C,EACD,SAAS,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,KAAK,SAAS,CAAC,EAAE,GAC/C,aAAa,CAAC,CAAC,EAAE,KAAK,CAAC,CAqDzB"}
|
package/dist/derive.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createSourceCore as e } from "./core.js";
|
|
2
|
+
//#region src/derive.ts
|
|
3
|
+
function t(t, n) {
|
|
4
|
+
let r = e(), i = [], a = () => {
|
|
5
|
+
i = n(t.current);
|
|
6
|
+
};
|
|
7
|
+
a();
|
|
8
|
+
let o = t.subscribe(() => {
|
|
9
|
+
a(), r.notify();
|
|
10
|
+
});
|
|
11
|
+
"disposalSignal" in t && t.disposalSignal instanceof AbortSignal && t.disposalSignal.addEventListener("abort", () => r.dispose(), { once: !0 });
|
|
12
|
+
let s = {
|
|
13
|
+
get current() {
|
|
14
|
+
return i;
|
|
15
|
+
},
|
|
16
|
+
get disposalSignal() {
|
|
17
|
+
return r.disposalSignal;
|
|
18
|
+
},
|
|
19
|
+
dispose() {
|
|
20
|
+
r.isDisposed || (o(), r.dispose());
|
|
21
|
+
},
|
|
22
|
+
get disposed() {
|
|
23
|
+
return r.isDisposed;
|
|
24
|
+
},
|
|
25
|
+
get meta() {
|
|
26
|
+
return t.meta;
|
|
27
|
+
},
|
|
28
|
+
subscribe(e) {
|
|
29
|
+
return r.subscribe(e);
|
|
30
|
+
},
|
|
31
|
+
[Symbol.dispose]() {
|
|
32
|
+
s.dispose();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
return s;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { t as deriveSource };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=derive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive.js","names":[],"sources":["../src/derive.ts"],"sourcesContent":["import type { DerivedSource } from './types';\n\nimport { createSourceCore } from './core';\n\n/**\n * Creates a reactive read-only projection derived from a parent source.\n *\n * The derived source mirrors the parent's `meta` and applies `transform` to `current`\n * whenever the parent changes. This is useful for client-side projections (e.g. additional\n * filtering, mapping, or sorting) over a remote source without re-fetching.\n *\n * Call `dispose()` to stop listening to the parent.\n *\n * @example\n * ```ts\n * const source = createRemoteSource<User>({ fetch: fetchUsers, limit: 20 });\n *\n * // Show only active users without re-fetching.\n * const activeUsers = deriveSource(source, (items) => items.filter((u) => u.active));\n *\n * activeUsers.subscribe(() => {\n * console.log(activeUsers.current, activeUsers.meta.totalItems);\n * });\n * ```\n */\nexport function deriveSource<T, U, TMeta>(\n parent: {\n readonly current: readonly T[];\n readonly meta: TMeta;\n subscribe(listener: () => void): () => void;\n },\n transform: (items: readonly T[]) => readonly U[],\n): DerivedSource<U, TMeta> {\n const core = createSourceCore();\n let cachedCurrent: readonly U[] = [];\n\n const applyTransform = () => {\n cachedCurrent = transform(parent.current);\n };\n\n applyTransform();\n\n const unsubscribeParent = parent.subscribe(() => {\n applyTransform();\n core.notify();\n });\n\n if ('disposalSignal' in parent && parent.disposalSignal instanceof AbortSignal) {\n parent.disposalSignal.addEventListener('abort', () => core.dispose(), { once: true });\n }\n\n const source: DerivedSource<U, TMeta> = {\n get current() {\n return cachedCurrent;\n },\n\n get disposalSignal() {\n return core.disposalSignal;\n },\n\n dispose() {\n if (core.isDisposed) return;\n\n unsubscribeParent();\n core.dispose();\n },\n\n get disposed() {\n return core.isDisposed;\n },\n\n get meta() {\n return parent.meta;\n },\n\n subscribe(listener) {\n return core.subscribe(listener);\n },\n\n [Symbol.dispose]() {\n source.dispose();\n },\n };\n\n return source;\n}\n"],"mappings":";;AAyBA,SAAgB,EACd,GAKA,GACyB;CACzB,IAAM,IAAO,EAAiB,GAC1B,IAA8B,CAAC,GAE7B,UAAuB;EAC3B,IAAgB,EAAU,EAAO,OAAO;CAC1C;CAEA,EAAe;CAEf,IAAM,IAAoB,EAAO,gBAAgB;EAE/C,AADA,EAAe,GACf,EAAK,OAAO;CACd,CAAC;CAED,AAAI,oBAAoB,KAAU,EAAO,0BAA0B,eACjE,EAAO,eAAe,iBAAiB,eAAe,EAAK,QAAQ,GAAG,EAAE,MAAM,GAAK,CAAC;CAGtF,IAAM,IAAkC;EACtC,IAAI,UAAU;GACZ,OAAO;EACT;EAEA,IAAI,iBAAiB;GACnB,OAAO,EAAK;EACd;EAEA,UAAU;GACJ,EAAK,eAET,EAAkB,GAClB,EAAK,QAAQ;EACf;EAEA,IAAI,WAAW;GACb,OAAO,EAAK;EACd;EAEA,IAAI,OAAO;GACT,OAAO,EAAO;EAChB;EAEA,UAAU,GAAU;GAClB,OAAO,EAAK,UAAU,CAAQ;EAChC;EAEA,CAAC,OAAO,WAAW;GACjB,EAAO,QAAQ;EACjB;CACF;CAEA,OAAO;AACT"}
|
package/dist/errors.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=class e extends Error{#e;constructor(e,t={}){super(e,t.cause===void 0?void 0:{cause:t.cause}),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype),this.#e=t}get attempt(){return this.#e.attempt??0}get context(){return this.#e.context}static is(t){return t instanceof e}},t=class extends e{timeoutMs;constructor(e){super(`Source.ready() timed out after ${e}ms`),this.timeoutMs=e}},n=class extends e{constructor(){super(`Source disposed while waiting for ready()`)}};exports.SourceDisposedError=n,exports.SourceTimeoutError=t,exports.SourcererError=e;
|
|
2
|
+
//# sourceMappingURL=errors.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.cjs","names":["#opts"],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Structured context carried by `SourcererError`.\n * Discriminated on `kind` — narrow with `error.context?.kind` to access typed fields.\n *\n * @example\n * ```ts\n * if (source.meta.error?.context?.kind === 'remote') {\n * console.log('Failed page', source.meta.error.context.page);\n * }\n * ```\n */\nexport type SourcererErrorContext =\n | Readonly<{ kind: 'cursor'; limit: number; search?: string }>\n | Readonly<{ kind: 'infinite'; limit: number; page: number; search?: string }>\n | Readonly<{ kind: 'remote'; limit: number; page: number; search?: string }>;\n\n/**\n * Base class for all sourcerer errors. Catch with `instanceof SourcererError` to handle any\n * sourcerer-originated error regardless of specific subtype.\n *\n * Carries the original cause, structured context, and the attempt number.\n *\n * @example\n * ```ts\n * if (source.meta.error) {\n * console.error(source.meta.error.message, source.meta.error.context);\n * }\n * ```\n */\nexport class SourcererError extends Error {\n readonly #opts: {\n readonly attempt?: number;\n readonly cause?: unknown;\n readonly context?: SourcererErrorContext;\n };\n\n constructor(\n message: string,\n opts: {\n readonly attempt?: number;\n readonly cause?: unknown;\n readonly context?: SourcererErrorContext;\n } = {},\n ) {\n super(message, opts.cause !== undefined ? { cause: opts.cause } : undefined);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n this.#opts = opts;\n }\n\n get attempt(): number {\n return this.#opts.attempt ?? 0;\n }\n\n /** Context bag for the error — safe to log. */\n get context(): SourcererErrorContext | undefined {\n return this.#opts.context;\n }\n\n static is(err: unknown): err is SourcererError {\n return err instanceof SourcererError;\n }\n}\n\n/** Thrown by `ready()` when the source timed out waiting for the first successful fetch. */\nexport class SourceTimeoutError extends SourcererError {\n /** The configured timeout in milliseconds. */\n readonly timeoutMs: number;\n\n constructor(timeoutMs: number) {\n super(`Source.ready() timed out after ${timeoutMs}ms`);\n this.timeoutMs = timeoutMs;\n }\n}\n\n/** Thrown by `ready()` when the source is disposed while waiting. */\nexport class SourceDisposedError extends SourcererError {\n constructor() {\n super('Source disposed while waiting for ready()');\n }\n}\n"],"mappings":"AA6BA,IAAa,EAAb,MAAa,UAAuB,KAAM,CACxC,GAMA,YACE,EACA,EAII,CAAC,EACL,CACA,MAAM,EAAS,EAAK,QAAU,IAAA,GAAoC,IAAA,GAAxB,CAAE,MAAO,EAAK,KAAM,CAAa,EAC3E,KAAK,KAAO,IAAI,OAAO,KACvB,OAAO,eAAe,KAAM,IAAI,OAAO,SAAS,EAChD,KAAKA,GAAQ,CACf,CAEA,IAAI,SAAkB,CACpB,OAAO,KAAKA,GAAM,SAAW,CAC/B,CAGA,IAAI,SAA6C,CAC/C,OAAO,KAAKA,GAAM,OACpB,CAEA,OAAO,GAAG,EAAqC,CAC7C,OAAO,aAAe,CACxB,CACF,EAGa,EAAb,cAAwC,CAAe,CAErD,UAEA,YAAY,EAAmB,CAC7B,MAAM,kCAAkC,EAAU,GAAG,EACrD,KAAK,UAAY,CACnB,CACF,EAGa,EAAb,cAAyC,CAAe,CACtD,aAAc,CACZ,MAAM,2CAA2C,CACnD,CACF"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured context carried by `SourcererError`.
|
|
3
|
+
* Discriminated on `kind` — narrow with `error.context?.kind` to access typed fields.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* if (source.meta.error?.context?.kind === 'remote') {
|
|
8
|
+
* console.log('Failed page', source.meta.error.context.page);
|
|
9
|
+
* }
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export type SourcererErrorContext = Readonly<{
|
|
13
|
+
kind: 'cursor';
|
|
14
|
+
limit: number;
|
|
15
|
+
search?: string;
|
|
16
|
+
}> | Readonly<{
|
|
17
|
+
kind: 'infinite';
|
|
18
|
+
limit: number;
|
|
19
|
+
page: number;
|
|
20
|
+
search?: string;
|
|
21
|
+
}> | Readonly<{
|
|
22
|
+
kind: 'remote';
|
|
23
|
+
limit: number;
|
|
24
|
+
page: number;
|
|
25
|
+
search?: string;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Base class for all sourcerer errors. Catch with `instanceof SourcererError` to handle any
|
|
29
|
+
* sourcerer-originated error regardless of specific subtype.
|
|
30
|
+
*
|
|
31
|
+
* Carries the original cause, structured context, and the attempt number.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* if (source.meta.error) {
|
|
36
|
+
* console.error(source.meta.error.message, source.meta.error.context);
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare class SourcererError extends Error {
|
|
41
|
+
#private;
|
|
42
|
+
constructor(message: string, opts?: {
|
|
43
|
+
readonly attempt?: number;
|
|
44
|
+
readonly cause?: unknown;
|
|
45
|
+
readonly context?: SourcererErrorContext;
|
|
46
|
+
});
|
|
47
|
+
get attempt(): number;
|
|
48
|
+
/** Context bag for the error — safe to log. */
|
|
49
|
+
get context(): SourcererErrorContext | undefined;
|
|
50
|
+
static is(err: unknown): err is SourcererError;
|
|
51
|
+
}
|
|
52
|
+
/** Thrown by `ready()` when the source timed out waiting for the first successful fetch. */
|
|
53
|
+
export declare class SourceTimeoutError extends SourcererError {
|
|
54
|
+
/** The configured timeout in milliseconds. */
|
|
55
|
+
readonly timeoutMs: number;
|
|
56
|
+
constructor(timeoutMs: number);
|
|
57
|
+
}
|
|
58
|
+
/** Thrown by `ready()` when the source is disposed while waiting. */
|
|
59
|
+
export declare class SourceDisposedError extends SourcererError {
|
|
60
|
+
constructor();
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,qBAAqB,GAC7B,QAAQ,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAC5D,QAAQ,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAC5E,QAAQ,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE/E;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAe,SAAQ,KAAK;;gBAQrC,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;QACJ,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC;KACrC;IAQR,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,+CAA+C;IAC/C,IAAI,OAAO,IAAI,qBAAqB,GAAG,SAAS,CAE/C;IAED,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,cAAc;CAG/C;AAED,4FAA4F;AAC5F,qBAAa,kBAAmB,SAAQ,cAAc;IACpD,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,SAAS,EAAE,MAAM;CAI9B;AAED,qEAAqE;AACrE,qBAAa,mBAAoB,SAAQ,cAAc;;CAItD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
var e = class e extends Error {
|
|
3
|
+
#e;
|
|
4
|
+
constructor(e, t = {}) {
|
|
5
|
+
super(e, t.cause === void 0 ? void 0 : { cause: t.cause }), this.name = new.target.name, Object.setPrototypeOf(this, new.target.prototype), this.#e = t;
|
|
6
|
+
}
|
|
7
|
+
get attempt() {
|
|
8
|
+
return this.#e.attempt ?? 0;
|
|
9
|
+
}
|
|
10
|
+
get context() {
|
|
11
|
+
return this.#e.context;
|
|
12
|
+
}
|
|
13
|
+
static is(t) {
|
|
14
|
+
return t instanceof e;
|
|
15
|
+
}
|
|
16
|
+
}, t = class extends e {
|
|
17
|
+
timeoutMs;
|
|
18
|
+
constructor(e) {
|
|
19
|
+
super(`Source.ready() timed out after ${e}ms`), this.timeoutMs = e;
|
|
20
|
+
}
|
|
21
|
+
}, n = class extends e {
|
|
22
|
+
constructor() {
|
|
23
|
+
super("Source disposed while waiting for ready()");
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { n as SourceDisposedError, t as SourceTimeoutError, e as SourcererError };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":["#opts"],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Structured context carried by `SourcererError`.\n * Discriminated on `kind` — narrow with `error.context?.kind` to access typed fields.\n *\n * @example\n * ```ts\n * if (source.meta.error?.context?.kind === 'remote') {\n * console.log('Failed page', source.meta.error.context.page);\n * }\n * ```\n */\nexport type SourcererErrorContext =\n | Readonly<{ kind: 'cursor'; limit: number; search?: string }>\n | Readonly<{ kind: 'infinite'; limit: number; page: number; search?: string }>\n | Readonly<{ kind: 'remote'; limit: number; page: number; search?: string }>;\n\n/**\n * Base class for all sourcerer errors. Catch with `instanceof SourcererError` to handle any\n * sourcerer-originated error regardless of specific subtype.\n *\n * Carries the original cause, structured context, and the attempt number.\n *\n * @example\n * ```ts\n * if (source.meta.error) {\n * console.error(source.meta.error.message, source.meta.error.context);\n * }\n * ```\n */\nexport class SourcererError extends Error {\n readonly #opts: {\n readonly attempt?: number;\n readonly cause?: unknown;\n readonly context?: SourcererErrorContext;\n };\n\n constructor(\n message: string,\n opts: {\n readonly attempt?: number;\n readonly cause?: unknown;\n readonly context?: SourcererErrorContext;\n } = {},\n ) {\n super(message, opts.cause !== undefined ? { cause: opts.cause } : undefined);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n this.#opts = opts;\n }\n\n get attempt(): number {\n return this.#opts.attempt ?? 0;\n }\n\n /** Context bag for the error — safe to log. */\n get context(): SourcererErrorContext | undefined {\n return this.#opts.context;\n }\n\n static is(err: unknown): err is SourcererError {\n return err instanceof SourcererError;\n }\n}\n\n/** Thrown by `ready()` when the source timed out waiting for the first successful fetch. */\nexport class SourceTimeoutError extends SourcererError {\n /** The configured timeout in milliseconds. */\n readonly timeoutMs: number;\n\n constructor(timeoutMs: number) {\n super(`Source.ready() timed out after ${timeoutMs}ms`);\n this.timeoutMs = timeoutMs;\n }\n}\n\n/** Thrown by `ready()` when the source is disposed while waiting. */\nexport class SourceDisposedError extends SourcererError {\n constructor() {\n super('Source disposed while waiting for ready()');\n }\n}\n"],"mappings":";AA6BA,IAAa,IAAb,MAAa,UAAuB,MAAM;CACxC;CAMA,YACE,GACA,IAII,CAAC,GACL;EAIA,AAHA,MAAM,GAAS,EAAK,UAAU,KAAA,IAAoC,KAAA,IAAxB,EAAE,OAAO,EAAK,MAAM,CAAa,GAC3E,KAAK,OAAO,IAAI,OAAO,MACvB,OAAO,eAAe,MAAM,IAAI,OAAO,SAAS,GAChD,KAAKA,KAAQ;CACf;CAEA,IAAI,UAAkB;EACpB,OAAO,KAAKA,GAAM,WAAW;CAC/B;CAGA,IAAI,UAA6C;EAC/C,OAAO,KAAKA,GAAM;CACpB;CAEA,OAAO,GAAG,GAAqC;EAC7C,OAAO,aAAe;CACxB;AACF,GAGa,IAAb,cAAwC,EAAe;CAErD;CAEA,YAAY,GAAmB;EAE7B,AADA,MAAM,kCAAkC,EAAU,GAAG,GACrD,KAAK,YAAY;CACnB;AACF,GAGa,IAAb,cAAyC,EAAe;CACtD,cAAc;EACZ,MAAM,2CAA2C;CACnD;AACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e){let t=new Map,n=``,r=0;return{dispose(){for(let e of t.values())e.controller.abort();t.clear(),r=0},get pendingCount(){return r},async run(i,a,o){let s=e(i);n=s;for(let[e,n]of t)e!==s&&(n.controller.abort(),t.delete(e));let c=t.get(s);if(c){r++,o();try{await c.promise}finally{r--,o()}return}let l=new AbortController;r++,o();let u=a(i,l.signal,()=>s===n).finally(()=>{t.delete(s),r--,o()});t.set(s,{controller:l,promise:u}),await u}}}exports.createFetchManager=e;
|
|
2
|
+
//# sourceMappingURL=fetchManager.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetchManager.cjs","names":[],"sources":["../src/fetchManager.ts"],"sourcesContent":["/**\n * Internal fetch deduplication and in-flight request manager.\n *\n * - Deduplicates identical concurrent queries: joiners await the existing in-flight promise.\n * - Aborts superseded in-flight requests when a new query key arrives.\n * - Tracks pending count so callers can reflect loading state.\n *\n * Not exported from the public API — consumed via `asyncSource.ts` by remoteSource,\n * cursorSource, and infiniteSource.\n */\n\ntype InFlightEntry = { controller: AbortController; promise: Promise<void> };\n\nexport type FetchManager<TQuery> = {\n /** Abort all in-flight requests and clear the registry. */\n dispose(): void;\n /** Number of currently in-flight requests (including joiners). */\n readonly pendingCount: number;\n /**\n * Run a fetch for query `q`.\n *\n * - If an identical key is already in-flight, joins as a waiter.\n * - If a different key is in-flight, aborts it and starts a new request.\n * - Calls `onPendingChange()` whenever `pendingCount` changes.\n * - Passes an `isLatest()` predicate to `execute` so it can skip stale responses.\n */\n run(\n q: TQuery,\n execute: (q: TQuery, signal: AbortSignal, isLatest: () => boolean) => Promise<void>,\n onPendingChange: () => void,\n ): Promise<void>;\n};\n\nexport function createFetchManager<TQuery>(keyOf: (q: TQuery) => string): FetchManager<TQuery> {\n const inflight = new Map<string, InFlightEntry>();\n let latestKey = '';\n let _pendingCount = 0;\n\n return {\n dispose() {\n for (const entry of inflight.values()) {\n entry.controller.abort();\n }\n\n inflight.clear();\n _pendingCount = 0;\n },\n\n get pendingCount() {\n return _pendingCount;\n },\n\n async run(q, execute, onPendingChange) {\n const key = keyOf(q);\n\n latestKey = key;\n\n // Abort and remove all superseded in-flight requests.\n for (const [k, entry] of inflight) {\n if (k !== key) {\n entry.controller.abort();\n inflight.delete(k);\n }\n }\n\n // Join an identical in-flight request rather than issuing a duplicate.\n const existing = inflight.get(key);\n\n if (existing) {\n _pendingCount++;\n onPendingChange();\n\n try {\n await existing.promise;\n } finally {\n _pendingCount--;\n onPendingChange();\n }\n\n return;\n }\n\n // New request.\n const controller = new AbortController();\n\n _pendingCount++;\n onPendingChange();\n\n const promise = execute(q, controller.signal, () => key === latestKey).finally(() => {\n inflight.delete(key);\n _pendingCount--;\n onPendingChange();\n });\n\n inflight.set(key, { controller, promise });\n await promise;\n },\n };\n}\n"],"mappings":"AAiCA,SAAgB,EAA2B,EAAoD,CAC7F,IAAM,EAAW,IAAI,IACjB,EAAY,GACZ,EAAgB,EAEpB,MAAO,CACL,SAAU,CACR,IAAK,IAAM,KAAS,EAAS,OAAO,EAClC,EAAM,WAAW,MAAM,EAGzB,EAAS,MAAM,EACf,EAAgB,CAClB,EAEA,IAAI,cAAe,CACjB,OAAO,CACT,EAEA,MAAM,IAAI,EAAG,EAAS,EAAiB,CACrC,IAAM,EAAM,EAAM,CAAC,EAEnB,EAAY,EAGZ,IAAK,GAAM,CAAC,EAAG,KAAU,EACnB,IAAM,IACR,EAAM,WAAW,MAAM,EACvB,EAAS,OAAO,CAAC,GAKrB,IAAM,EAAW,EAAS,IAAI,CAAG,EAEjC,GAAI,EAAU,CACZ,IACA,EAAgB,EAEhB,GAAI,CACF,MAAM,EAAS,OACjB,QAAU,CACR,IACA,EAAgB,CAClB,CAEA,MACF,CAGA,IAAM,EAAa,IAAI,gBAEvB,IACA,EAAgB,EAEhB,IAAM,EAAU,EAAQ,EAAG,EAAW,WAAc,IAAQ,CAAS,CAAC,CAAC,YAAc,CACnF,EAAS,OAAO,CAAG,EACnB,IACA,EAAgB,CAClB,CAAC,EAED,EAAS,IAAI,EAAK,CAAE,aAAY,SAAQ,CAAC,EACzC,MAAM,CACR,CACF,CACF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal fetch deduplication and in-flight request manager.
|
|
3
|
+
*
|
|
4
|
+
* - Deduplicates identical concurrent queries: joiners await the existing in-flight promise.
|
|
5
|
+
* - Aborts superseded in-flight requests when a new query key arrives.
|
|
6
|
+
* - Tracks pending count so callers can reflect loading state.
|
|
7
|
+
*
|
|
8
|
+
* Not exported from the public API — consumed via `asyncSource.ts` by remoteSource,
|
|
9
|
+
* cursorSource, and infiniteSource.
|
|
10
|
+
*/
|
|
11
|
+
export type FetchManager<TQuery> = {
|
|
12
|
+
/** Abort all in-flight requests and clear the registry. */
|
|
13
|
+
dispose(): void;
|
|
14
|
+
/** Number of currently in-flight requests (including joiners). */
|
|
15
|
+
readonly pendingCount: number;
|
|
16
|
+
/**
|
|
17
|
+
* Run a fetch for query `q`.
|
|
18
|
+
*
|
|
19
|
+
* - If an identical key is already in-flight, joins as a waiter.
|
|
20
|
+
* - If a different key is in-flight, aborts it and starts a new request.
|
|
21
|
+
* - Calls `onPendingChange()` whenever `pendingCount` changes.
|
|
22
|
+
* - Passes an `isLatest()` predicate to `execute` so it can skip stale responses.
|
|
23
|
+
*/
|
|
24
|
+
run(q: TQuery, execute: (q: TQuery, signal: AbortSignal, isLatest: () => boolean) => Promise<void>, onPendingChange: () => void): Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
export declare function createFetchManager<TQuery>(keyOf: (q: TQuery) => string): FetchManager<TQuery>;
|
|
27
|
+
//# sourceMappingURL=fetchManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetchManager.d.ts","sourceRoot":"","sources":["../src/fetchManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,MAAM,YAAY,CAAC,MAAM,IAAI;IACjC,2DAA2D;IAC3D,OAAO,IAAI,IAAI,CAAC;IAChB,kEAAkE;IAClE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,GAAG,CACD,CAAC,EAAE,MAAM,EACT,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,EACnF,eAAe,EAAE,MAAM,IAAI,GAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAiE7F"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/fetchManager.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
let t = /* @__PURE__ */ new Map(), n = "", r = 0;
|
|
4
|
+
return {
|
|
5
|
+
dispose() {
|
|
6
|
+
for (let e of t.values()) e.controller.abort();
|
|
7
|
+
t.clear(), r = 0;
|
|
8
|
+
},
|
|
9
|
+
get pendingCount() {
|
|
10
|
+
return r;
|
|
11
|
+
},
|
|
12
|
+
async run(i, a, o) {
|
|
13
|
+
let s = e(i);
|
|
14
|
+
n = s;
|
|
15
|
+
for (let [e, n] of t) e !== s && (n.controller.abort(), t.delete(e));
|
|
16
|
+
let c = t.get(s);
|
|
17
|
+
if (c) {
|
|
18
|
+
r++, o();
|
|
19
|
+
try {
|
|
20
|
+
await c.promise;
|
|
21
|
+
} finally {
|
|
22
|
+
r--, o();
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let l = new AbortController();
|
|
27
|
+
r++, o();
|
|
28
|
+
let u = a(i, l.signal, () => s === n).finally(() => {
|
|
29
|
+
t.delete(s), r--, o();
|
|
30
|
+
});
|
|
31
|
+
t.set(s, {
|
|
32
|
+
controller: l,
|
|
33
|
+
promise: u
|
|
34
|
+
}), await u;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { e as createFetchManager };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=fetchManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetchManager.js","names":[],"sources":["../src/fetchManager.ts"],"sourcesContent":["/**\n * Internal fetch deduplication and in-flight request manager.\n *\n * - Deduplicates identical concurrent queries: joiners await the existing in-flight promise.\n * - Aborts superseded in-flight requests when a new query key arrives.\n * - Tracks pending count so callers can reflect loading state.\n *\n * Not exported from the public API — consumed via `asyncSource.ts` by remoteSource,\n * cursorSource, and infiniteSource.\n */\n\ntype InFlightEntry = { controller: AbortController; promise: Promise<void> };\n\nexport type FetchManager<TQuery> = {\n /** Abort all in-flight requests and clear the registry. */\n dispose(): void;\n /** Number of currently in-flight requests (including joiners). */\n readonly pendingCount: number;\n /**\n * Run a fetch for query `q`.\n *\n * - If an identical key is already in-flight, joins as a waiter.\n * - If a different key is in-flight, aborts it and starts a new request.\n * - Calls `onPendingChange()` whenever `pendingCount` changes.\n * - Passes an `isLatest()` predicate to `execute` so it can skip stale responses.\n */\n run(\n q: TQuery,\n execute: (q: TQuery, signal: AbortSignal, isLatest: () => boolean) => Promise<void>,\n onPendingChange: () => void,\n ): Promise<void>;\n};\n\nexport function createFetchManager<TQuery>(keyOf: (q: TQuery) => string): FetchManager<TQuery> {\n const inflight = new Map<string, InFlightEntry>();\n let latestKey = '';\n let _pendingCount = 0;\n\n return {\n dispose() {\n for (const entry of inflight.values()) {\n entry.controller.abort();\n }\n\n inflight.clear();\n _pendingCount = 0;\n },\n\n get pendingCount() {\n return _pendingCount;\n },\n\n async run(q, execute, onPendingChange) {\n const key = keyOf(q);\n\n latestKey = key;\n\n // Abort and remove all superseded in-flight requests.\n for (const [k, entry] of inflight) {\n if (k !== key) {\n entry.controller.abort();\n inflight.delete(k);\n }\n }\n\n // Join an identical in-flight request rather than issuing a duplicate.\n const existing = inflight.get(key);\n\n if (existing) {\n _pendingCount++;\n onPendingChange();\n\n try {\n await existing.promise;\n } finally {\n _pendingCount--;\n onPendingChange();\n }\n\n return;\n }\n\n // New request.\n const controller = new AbortController();\n\n _pendingCount++;\n onPendingChange();\n\n const promise = execute(q, controller.signal, () => key === latestKey).finally(() => {\n inflight.delete(key);\n _pendingCount--;\n onPendingChange();\n });\n\n inflight.set(key, { controller, promise });\n await promise;\n },\n };\n}\n"],"mappings":";AAiCA,SAAgB,EAA2B,GAAoD;CAC7F,IAAM,oBAAW,IAAI,IAA2B,GAC5C,IAAY,IACZ,IAAgB;CAEpB,OAAO;EACL,UAAU;GACR,KAAK,IAAM,KAAS,EAAS,OAAO,GAClC,EAAM,WAAW,MAAM;GAIzB,AADA,EAAS,MAAM,GACf,IAAgB;EAClB;EAEA,IAAI,eAAe;GACjB,OAAO;EACT;EAEA,MAAM,IAAI,GAAG,GAAS,GAAiB;GACrC,IAAM,IAAM,EAAM,CAAC;GAEnB,IAAY;GAGZ,KAAK,IAAM,CAAC,GAAG,MAAU,GACvB,AAAI,MAAM,MACR,EAAM,WAAW,MAAM,GACvB,EAAS,OAAO,CAAC;GAKrB,IAAM,IAAW,EAAS,IAAI,CAAG;GAEjC,IAAI,GAAU;IAEZ,AADA,KACA,EAAgB;IAEhB,IAAI;KACF,MAAM,EAAS;IACjB,UAAU;KAER,AADA,KACA,EAAgB;IAClB;IAEA;GACF;GAGA,IAAM,IAAa,IAAI,gBAAgB;GAGvC,AADA,KACA,EAAgB;GAEhB,IAAM,IAAU,EAAQ,GAAG,EAAW,cAAc,MAAQ,CAAS,CAAC,CAAC,cAAc;IAGnF,AAFA,EAAS,OAAO,CAAG,GACnB,KACA,EAAgB;GAClB,CAAC;GAGD,AADA,EAAS,IAAI,GAAK;IAAE;IAAY;GAAQ,CAAC,GACzC,MAAM;EACR;CACF;AACF"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./applyQuery.cjs"),t=require("./errors.cjs"),n=require("./codecs.cjs"),r=require("./cursorSource.cjs"),i=require("./derive.cjs"),a=require("./infiniteSource.cjs"),o=require("./pagination.cjs"),s=require("./localSource.cjs"),c=require("./merge.cjs"),l=require("./remoteSource.cjs"),u=require("./prefetch.cjs"),d=require("./presets.cjs"),f=require("./state.cjs");exports.SourceDisposedError=t.SourceDisposedError,exports.SourceTimeoutError=t.SourceTimeoutError,exports.SourcererError=t.SourcererError,exports.applyQuery=e.applyQuery,exports.createCursorSource=r.createCursorSource,exports.createInfiniteSource=a.createInfiniteSource,exports.createLocalSource=s.createLocalSource,exports.createRemoteSource=l.createRemoteSource,exports.decodeQuery=n.decodeQuery,exports.deriveSource=i.deriveSource,exports.encodeQuery=n.encodeQuery,exports.filterContains=d.filterContains,exports.filterEquals=d.filterEquals,exports.filterRange=d.filterRange,exports.itemRange=o.itemRange,exports.mergeSource=c.mergeSource,exports.prefetchSource=u.prefetchSource,exports.prefetchSourceAndKeep=u.prefetchSourceAndKeep,exports.sortBy=d.sortBy,exports.sourceState=f.sourceState;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { applyQuery } from './applyQuery';
|
|
2
|
+
export { decodeQuery, encodeQuery } from './codecs';
|
|
3
|
+
export type { DecodeQueryOptions } from './codecs';
|
|
4
|
+
export { createCursorSource } from './cursorSource';
|
|
5
|
+
export { deriveSource } from './derive';
|
|
6
|
+
export { createInfiniteSource } from './infiniteSource';
|
|
7
|
+
export { createLocalSource } from './localSource';
|
|
8
|
+
export { mergeSource } from './merge';
|
|
9
|
+
export { itemRange } from './pagination';
|
|
10
|
+
export { prefetchSource, prefetchSourceAndKeep } from './prefetch';
|
|
11
|
+
export { filterContains, filterEquals, filterRange, sortBy } from './presets';
|
|
12
|
+
export { createRemoteSource } from './remoteSource';
|
|
13
|
+
export { sourceState } from './state';
|
|
14
|
+
export { SourceDisposedError, SourcererError, SourceTimeoutError } from './errors';
|
|
15
|
+
export type { CursorConfig, CursorMeta, CursorSource, CursorSourceQuery, DerivedSource, FetchEvent, InfiniteConfig, InfiniteMeta, InfiniteSource, InfiniteSourceQuery, LocalSource, LocalSourceConfig, LocalSourceQuery, MergedSource, PageNavigator, Predicate, QueryParams, QueryParamsInput, ReactiveSource, RemoteConfig, RemoteSource, RemoteSourceQuery, RetryConfig, SearchOptions, Sorter, SourcererErrorContext, SourceMeta, SourceQuery, SourceSnapshot, SourceState, } from './types';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACpD,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnF,YAAY,EACV,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,cAAc,EACd,YAAY,EACZ,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,SAAS,EACT,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,MAAM,EACN,qBAAqB,EACrB,UAAU,EACV,WAAW,EACX,cAAc,EACd,WAAW,GACZ,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { applyQuery as e } from "./applyQuery.js";
|
|
2
|
+
import { SourceDisposedError as t, SourceTimeoutError as n, SourcererError as r } from "./errors.js";
|
|
3
|
+
import { decodeQuery as i, encodeQuery as a } from "./codecs.js";
|
|
4
|
+
import { createCursorSource as o } from "./cursorSource.js";
|
|
5
|
+
import { deriveSource as s } from "./derive.js";
|
|
6
|
+
import { createInfiniteSource as c } from "./infiniteSource.js";
|
|
7
|
+
import { itemRange as l } from "./pagination.js";
|
|
8
|
+
import { createLocalSource as u } from "./localSource.js";
|
|
9
|
+
import { mergeSource as d } from "./merge.js";
|
|
10
|
+
import { createRemoteSource as f } from "./remoteSource.js";
|
|
11
|
+
import { prefetchSource as p, prefetchSourceAndKeep as m } from "./prefetch.js";
|
|
12
|
+
import { filterContains as h, filterEquals as g, filterRange as _, sortBy as v } from "./presets.js";
|
|
13
|
+
import { sourceState as y } from "./state.js";
|
|
14
|
+
export { t as SourceDisposedError, n as SourceTimeoutError, r as SourcererError, e as applyQuery, o as createCursorSource, c as createInfiniteSource, u as createLocalSource, f as createRemoteSource, i as decodeQuery, s as deriveSource, a as encodeQuery, h as filterContains, g as filterEquals, _ as filterRange, l as itemRange, d as mergeSource, p as prefetchSource, m as prefetchSourceAndKeep, v as sortBy, y as sourceState };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./errors.cjs"),t=require("./_utils.cjs"),n=require("./asyncSource.cjs");let r=require("@vielzeug/arsenal");function i(i){let a=i.queryKey??t.defaultKeyOf,o=Math.max(1,Math.trunc(i.limit??20)),s=``,c=1,l=0,u=[],d=0,f=null,p=!1,m=null,h=[],g={error:null,hasMore:!1,isLoading:!1,isLoadingMore:!1,isSearchPending:!1,loadedPages:0,pageSize:o,totalItems:0},_=()=>{h=u,g={error:f,hasMore:u.length<d,isLoading:v.pendingCount()>0&&!p,isLoadingMore:p,isSearchPending:v.core.isScheduled,loadedPages:l,pageSize:o,totalItems:d}},v=n.createAsyncSource(i,a,_),{autoFetch:y,debounceMs:b,retryAttempts:x,retryDelay:S}=v;_();let C=()=>{v.disposed||v.core.notify()},w=()=>{m&&=(m.resolve(),null)},T=(n,a)=>{let m={...s&&{search:s},limit:o,page:n};return f=null,v.fetch(m,async(o,s,m)=>{let h=Date.now();try{let e=await(0,r.retry)(e=>i.fetch(o,e),{delay:S,signal:s,times:x+1});m()&&(u=a?[...u,...e.items]:e.items,d=e.total,c=n,l=a?l+1:1,f=null,i.onFetch?.({durationMs:Date.now()-h,query:o,status:`success`}))}catch(n){if(s.aborted)return;m()&&(a||(u=[]),d=a?d:0,f=new e.SourcererError(t.extractError(n),{cause:n,context:{kind:`infinite`,limit:o.limit,page:o.page,...o.search&&{search:o.search}}}),i.onFetch?.({durationMs:Date.now()-h,error:f,query:o,status:`error`}))}finally{p=!1}},()=>{C(),v.pendingCount()===0&&w()})},E=()=>T(1,!1);return y&&E(),v.startRefreshInterval(()=>void E()),{get current(){return h},get disposalSignal(){return v.disposalSignal},dispose:()=>{w(),v.dispose()},get disposed(){return v.disposed},loadMore(){return!g.hasMore||v.pendingCount()>0||p?Promise.resolve():(p=!0,_(),T(c+1,!0))},get meta(){return g},patch(e){let t=!1;if(e.limit!==void 0){let n=Math.max(1,Math.trunc(e.limit));n!==o&&(o=n,t=!0)}return`search`in e&&(e.search!==s||v.core.isScheduled)&&(s=e.search??``,t=!0),t?(v.core.cancelTimer(),w(),u=[],d=0,c=1,l=0,E()):Promise.resolve()},get query(){return{...s&&{search:s},limit:o,page:c}},ready(e){return v.ready(e)},reset(){return v.core.cancelTimer(),w(),c=1,l=0,u=[],d=0,s=``,E()},search(e,t){if(t?.immediate)return e===s&&!v.core.isScheduled?Promise.resolve():(v.core.cancelTimer(),w(),s=e,u=[],d=0,l=0,c=1,E());if(e===s)return Promise.resolve();w(),s=e,u=[],d=0,l=0,c=1;let n,r=new Promise(e=>{n=e});return m={promise:r,resolve:n},v.core.schedule(()=>void E(),b),C(),r},subscribe:e=>v.core.subscribe(e),[Symbol.dispose]:()=>{w(),v.dispose()}}}exports.createInfiniteSource=i;
|
|
2
|
+
//# sourceMappingURL=infiniteSource.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"infiniteSource.cjs","names":[],"sources":["../src/infiniteSource.ts"],"sourcesContent":["import type { InfiniteConfig, InfiniteMeta, InfiniteSource, InfiniteSourceQuery, SearchOptions } from './types';\n\nimport { defaultKeyOf, extractError, retry } from './_utils';\nimport { createAsyncSource } from './asyncSource';\nimport { SourcererError } from './errors';\n\ntype PendingSearch = { promise: Promise<void>; resolve: () => void };\n\n/** Creates an infinite-scroll source that appends pages on `loadMore()`. */\nexport function createInfiniteSource<T>(cfg: InfiniteConfig<T>): InfiniteSource<T> {\n const keyOf = cfg.queryKey ?? defaultKeyOf;\n\n // ── Mutable state ───────────────────────────────────────────────────────────\n let limit = Math.max(1, Math.trunc(cfg.limit ?? 20));\n let search = '';\n let currentPage = 1;\n let loadedPages = 0;\n let items: readonly T[] = [];\n let total = 0;\n let error: SourcererError | null = null;\n let isLoadingMore = false;\n let pendingSearch: PendingSearch | null = null;\n\n // ── Cached accessors ─────────────────────────────────────────────────────────\n let cachedCurrent: readonly T[] = [];\n let cachedMeta: InfiniteMeta = {\n error: null,\n hasMore: false,\n isLoading: false,\n isLoadingMore: false,\n isSearchPending: false,\n loadedPages: 0,\n pageSize: limit,\n totalItems: 0,\n };\n\n const refreshMeta = () => {\n cachedCurrent = items;\n cachedMeta = {\n error,\n hasMore: items.length < total,\n isLoading: base.pendingCount() > 0 && !isLoadingMore,\n isLoadingMore,\n isSearchPending: base.core.isScheduled,\n loadedPages,\n pageSize: limit,\n totalItems: total,\n };\n };\n\n // onBeforeNotify ensures refreshMeta() runs before any subscriber observes the new state,\n // eliminating the need for a parallel listeners Set.\n const base = createAsyncSource<InfiniteSourceQuery>(cfg, keyOf, refreshMeta);\n const { autoFetch, debounceMs, retryAttempts, retryDelay } = base;\n\n refreshMeta();\n\n const notifyListeners = () => {\n if (base.disposed) return;\n\n base.core.notify();\n };\n\n const resolvePendingSearch = () => {\n if (pendingSearch) {\n pendingSearch.resolve();\n pendingSearch = null;\n }\n };\n\n // ── Core fetch ──────────────────────────────────────────────────────────────\n const fetchPage = (targetPage: number, append: boolean): Promise<void> => {\n const q: InfiniteSourceQuery = {\n ...(search && { search }),\n limit,\n page: targetPage,\n };\n\n error = null;\n\n return base.fetch(\n q,\n async (q, signal, isLatest) => {\n const startTime = Date.now();\n\n try {\n const result = await retry((sig) => cfg.fetch(q, sig!), {\n delay: retryDelay,\n signal,\n times: retryAttempts + 1,\n });\n\n if (isLatest()) {\n if (append) {\n items = [...items, ...result.items];\n } else {\n items = result.items;\n }\n\n total = result.total;\n currentPage = targetPage;\n loadedPages = append ? loadedPages + 1 : 1;\n error = null;\n cfg.onFetch?.({ durationMs: Date.now() - startTime, query: q, status: 'success' });\n }\n } catch (reason: unknown) {\n if (signal.aborted) return;\n\n if (isLatest()) {\n if (!append) items = [];\n\n total = append ? total : 0;\n error = new SourcererError(extractError(reason), {\n cause: reason,\n context: { kind: 'infinite', limit: q.limit, page: q.page, ...(q.search && { search: q.search }) },\n });\n cfg.onFetch?.({ durationMs: Date.now() - startTime, error, query: q, status: 'error' });\n }\n } finally {\n isLoadingMore = false;\n }\n },\n () => {\n notifyListeners();\n\n if (base.pendingCount() === 0) resolvePendingSearch();\n },\n );\n };\n\n const doFetch = () => fetchPage(1, false);\n\n // ── Auto-fetch + refresh interval ───────────────────────────────────────────\n if (autoFetch) void doFetch();\n\n base.startRefreshInterval(() => void doFetch());\n\n // ── Public API ──────────────────────────────────────────────────────────────\n return {\n get current() {\n return cachedCurrent;\n },\n\n get disposalSignal() {\n return base.disposalSignal;\n },\n\n dispose: () => {\n resolvePendingSearch();\n base.dispose();\n },\n\n get disposed() {\n return base.disposed;\n },\n\n loadMore() {\n if (!cachedMeta.hasMore || base.pendingCount() > 0 || isLoadingMore) return Promise.resolve();\n\n isLoadingMore = true;\n refreshMeta();\n\n return fetchPage(currentPage + 1, true);\n },\n\n get meta() {\n return cachedMeta;\n },\n\n patch(changes) {\n let changed = false;\n\n if (changes.limit !== undefined) {\n const next = Math.max(1, Math.trunc(changes.limit));\n\n if (next !== limit) {\n limit = next;\n changed = true;\n }\n }\n\n // `|| base.core.isScheduled` also catches patching in the same search text as an already-pending\n // debounced search() call — that still needs flushing, not silently dropping as a no-op.\n if ('search' in changes && (changes.search !== search || base.core.isScheduled)) {\n search = changes.search ?? '';\n changed = true;\n }\n\n if (!changed) return Promise.resolve();\n\n // patch() is documented as a single atomic fetch — cancel any debounced search()\n // still pending so it doesn't fire a second, redundant fetch afterwards.\n base.core.cancelTimer();\n resolvePendingSearch();\n\n items = [];\n total = 0;\n currentPage = 1;\n loadedPages = 0;\n\n return doFetch();\n },\n\n get query() {\n return {\n ...(search && { search }),\n limit,\n page: currentPage,\n };\n },\n\n ready(timeout) {\n return base.ready(timeout);\n },\n\n reset() {\n base.core.cancelTimer();\n resolvePendingSearch();\n currentPage = 1;\n loadedPages = 0;\n items = [];\n total = 0;\n search = '';\n\n return doFetch();\n },\n\n search(q, opts?: SearchOptions): Promise<void> {\n if (opts?.immediate) {\n // A pending debounced search for this same text still needs flushing —\n // `q === search` alone doesn't mean the fetch already happened.\n if (q === search && !base.core.isScheduled) return Promise.resolve();\n\n base.core.cancelTimer();\n resolvePendingSearch();\n search = q;\n items = [];\n total = 0;\n loadedPages = 0;\n currentPage = 1;\n\n return doFetch();\n }\n\n if (q === search) return Promise.resolve();\n\n resolvePendingSearch();\n\n search = q;\n items = [];\n total = 0;\n loadedPages = 0;\n currentPage = 1;\n\n let resolveSearch!: () => void;\n const promise = new Promise<void>((res) => {\n resolveSearch = res;\n });\n\n pendingSearch = { promise, resolve: resolveSearch };\n\n base.core.schedule(() => void doFetch(), debounceMs);\n notifyListeners();\n\n return promise;\n },\n\n subscribe: (listener) => base.core.subscribe(listener),\n\n [Symbol.dispose]: () => {\n resolvePendingSearch();\n base.dispose();\n },\n };\n}\n"],"mappings":"4HASA,SAAgB,EAAwB,EAA2C,CACjF,IAAM,EAAQ,EAAI,UAAY,EAAA,aAG1B,EAAQ,KAAK,IAAI,EAAG,KAAK,MAAM,EAAI,OAAS,EAAE,CAAC,EAC/C,EAAS,GACT,EAAc,EACd,EAAc,EACd,EAAsB,CAAC,EACvB,EAAQ,EACR,EAA+B,KAC/B,EAAgB,GAChB,EAAsC,KAGtC,EAA8B,CAAC,EAC/B,EAA2B,CAC7B,MAAO,KACP,QAAS,GACT,UAAW,GACX,cAAe,GACf,gBAAiB,GACjB,YAAa,EACb,SAAU,EACV,WAAY,CACd,EAEM,MAAoB,CACxB,EAAgB,EAChB,EAAa,CACX,QACA,QAAS,EAAM,OAAS,EACxB,UAAW,EAAK,aAAa,EAAI,GAAK,CAAC,EACvC,gBACA,gBAAiB,EAAK,KAAK,YAC3B,cACA,SAAU,EACV,WAAY,CACd,CACF,EAIM,EAAO,EAAA,kBAAuC,EAAK,EAAO,CAAW,EACrE,CAAE,YAAW,aAAY,gBAAe,cAAe,EAE7D,EAAY,EAEZ,IAAM,MAAwB,CACxB,EAAK,UAET,EAAK,KAAK,OAAO,CACnB,EAEM,MAA6B,CACjC,AAEE,KADA,EAAc,QAAQ,EACN,KAEpB,EAGM,GAAa,EAAoB,IAAmC,CACxE,IAAM,EAAyB,CAC7B,GAAI,GAAU,CAAE,QAAO,EACvB,QACA,KAAM,CACR,EAIA,MAFA,GAAQ,KAED,EAAK,MACV,EACA,MAAO,EAAG,EAAQ,IAAa,CAC7B,IAAM,EAAY,KAAK,IAAI,EAE3B,GAAI,CACF,IAAM,EAAS,MAAA,EAAA,EAAA,MAAA,CAAa,GAAQ,EAAI,MAAM,EAAG,CAAI,EAAG,CACtD,MAAO,EACP,SACA,MAAO,EAAgB,CACzB,CAAC,EAEG,EAAS,IACX,AAGE,EAHE,EACM,CAAC,GAAG,EAAO,GAAG,EAAO,KAAK,EAE1B,EAAO,MAGjB,EAAQ,EAAO,MACf,EAAc,EACd,EAAc,EAAS,EAAc,EAAI,EACzC,EAAQ,KACR,EAAI,UAAU,CAAE,WAAY,KAAK,IAAI,EAAI,EAAW,MAAO,EAAG,OAAQ,SAAU,CAAC,EAErF,OAAS,EAAiB,CACxB,GAAI,EAAO,QAAS,OAEhB,EAAS,IACN,IAAQ,EAAQ,CAAC,GAEtB,EAAQ,EAAS,EAAQ,EACzB,EAAQ,IAAI,EAAA,eAAe,EAAA,aAAa,CAAM,EAAG,CAC/C,MAAO,EACP,QAAS,CAAE,KAAM,WAAY,MAAO,EAAE,MAAO,KAAM,EAAE,KAAM,GAAI,EAAE,QAAU,CAAE,OAAQ,EAAE,MAAO,CAAG,CACnG,CAAC,EACD,EAAI,UAAU,CAAE,WAAY,KAAK,IAAI,EAAI,EAAW,QAAO,MAAO,EAAG,OAAQ,OAAQ,CAAC,EAE1F,QAAU,CACR,EAAgB,EAClB,CACF,MACM,CACJ,EAAgB,EAEZ,EAAK,aAAa,IAAM,GAAG,EAAqB,CACtD,CACF,CACF,EAEM,MAAgB,EAAU,EAAG,EAAK,EAQxC,OALI,GAAW,EAAa,EAE5B,EAAK,yBAA2B,KAAK,EAAQ,CAAC,EAGvC,CACL,IAAI,SAAU,CACZ,OAAO,CACT,EAEA,IAAI,gBAAiB,CACnB,OAAO,EAAK,cACd,EAEA,YAAe,CACb,EAAqB,EACrB,EAAK,QAAQ,CACf,EAEA,IAAI,UAAW,CACb,OAAO,EAAK,QACd,EAEA,UAAW,CAMT,MALI,CAAC,EAAW,SAAW,EAAK,aAAa,EAAI,GAAK,EAAsB,QAAQ,QAAQ,GAE5F,EAAgB,GAChB,EAAY,EAEL,EAAU,EAAc,EAAG,EAAI,EACxC,EAEA,IAAI,MAAO,CACT,OAAO,CACT,EAEA,MAAM,EAAS,CACb,IAAI,EAAU,GAEd,GAAI,EAAQ,QAAU,IAAA,GAAW,CAC/B,IAAM,EAAO,KAAK,IAAI,EAAG,KAAK,MAAM,EAAQ,KAAK,CAAC,EAE9C,IAAS,IACX,EAAQ,EACR,EAAU,GAEd,CAqBA,MAjBI,WAAY,IAAY,EAAQ,SAAW,GAAU,EAAK,KAAK,eACjE,EAAS,EAAQ,QAAU,GAC3B,EAAU,IAGP,GAIL,EAAK,KAAK,YAAY,EACtB,EAAqB,EAErB,EAAQ,CAAC,EACT,EAAQ,EACR,EAAc,EACd,EAAc,EAEP,EAAQ,GAZM,QAAQ,QAAQ,CAavC,EAEA,IAAI,OAAQ,CACV,MAAO,CACL,GAAI,GAAU,CAAE,QAAO,EACvB,QACA,KAAM,CACR,CACF,EAEA,MAAM,EAAS,CACb,OAAO,EAAK,MAAM,CAAO,CAC3B,EAEA,OAAQ,CASN,OARA,EAAK,KAAK,YAAY,EACtB,EAAqB,EACrB,EAAc,EACd,EAAc,EACd,EAAQ,CAAC,EACT,EAAQ,EACR,EAAS,GAEF,EAAQ,CACjB,EAEA,OAAO,EAAG,EAAqC,CAC7C,GAAI,GAAM,UAaR,OAVI,IAAM,GAAU,CAAC,EAAK,KAAK,YAAoB,QAAQ,QAAQ,GAEnE,EAAK,KAAK,YAAY,EACtB,EAAqB,EACrB,EAAS,EACT,EAAQ,CAAC,EACT,EAAQ,EACR,EAAc,EACd,EAAc,EAEP,EAAQ,GAGjB,GAAI,IAAM,EAAQ,OAAO,QAAQ,QAAQ,EAEzC,EAAqB,EAErB,EAAS,EACT,EAAQ,CAAC,EACT,EAAQ,EACR,EAAc,EACd,EAAc,EAEd,IAAI,EACE,EAAU,IAAI,QAAe,GAAQ,CACzC,EAAgB,CAClB,CAAC,EAOD,MALA,GAAgB,CAAE,UAAS,QAAS,CAAc,EAElD,EAAK,KAAK,aAAe,KAAK,EAAQ,EAAG,CAAU,EACnD,EAAgB,EAET,CACT,EAEA,UAAY,GAAa,EAAK,KAAK,UAAU,CAAQ,GAEpD,OAAO,aAAgB,CACtB,EAAqB,EACrB,EAAK,QAAQ,CACf,CACF,CACF"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { InfiniteConfig, InfiniteSource } from './types';
|
|
2
|
+
/** Creates an infinite-scroll source that appends pages on `loadMore()`. */
|
|
3
|
+
export declare function createInfiniteSource<T>(cfg: InfiniteConfig<T>): InfiniteSource<T>;
|
|
4
|
+
//# sourceMappingURL=infiniteSource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"infiniteSource.d.ts","sourceRoot":"","sources":["../src/infiniteSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAgB,cAAc,EAAsC,MAAM,SAAS,CAAC;AAQhH,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAyQjF"}
|