@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,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/infiniteSource.ts
|
|
5
|
+
function a(a) {
|
|
6
|
+
let o = a.queryKey ?? t, s = Math.max(1, Math.trunc(a.limit ?? 20)), c = "", l = 1, u = 0, d = [], f = 0, p = null, m = !1, h = null, g = [], _ = {
|
|
7
|
+
error: null,
|
|
8
|
+
hasMore: !1,
|
|
9
|
+
isLoading: !1,
|
|
10
|
+
isLoadingMore: !1,
|
|
11
|
+
isSearchPending: !1,
|
|
12
|
+
loadedPages: 0,
|
|
13
|
+
pageSize: s,
|
|
14
|
+
totalItems: 0
|
|
15
|
+
}, v = () => {
|
|
16
|
+
g = d, _ = {
|
|
17
|
+
error: p,
|
|
18
|
+
hasMore: d.length < f,
|
|
19
|
+
isLoading: y.pendingCount() > 0 && !m,
|
|
20
|
+
isLoadingMore: m,
|
|
21
|
+
isSearchPending: y.core.isScheduled,
|
|
22
|
+
loadedPages: u,
|
|
23
|
+
pageSize: s,
|
|
24
|
+
totalItems: f
|
|
25
|
+
};
|
|
26
|
+
}, y = i(a, o, v), { autoFetch: b, debounceMs: x, retryAttempts: S, retryDelay: C } = y;
|
|
27
|
+
v();
|
|
28
|
+
let w = () => {
|
|
29
|
+
y.disposed || y.core.notify();
|
|
30
|
+
}, T = () => {
|
|
31
|
+
h &&= (h.resolve(), null);
|
|
32
|
+
}, E = (t, i) => {
|
|
33
|
+
let o = {
|
|
34
|
+
...c && { search: c },
|
|
35
|
+
limit: s,
|
|
36
|
+
page: t
|
|
37
|
+
};
|
|
38
|
+
return p = null, y.fetch(o, async (o, s, c) => {
|
|
39
|
+
let h = Date.now();
|
|
40
|
+
try {
|
|
41
|
+
let e = await r((e) => a.fetch(o, e), {
|
|
42
|
+
delay: C,
|
|
43
|
+
signal: s,
|
|
44
|
+
times: S + 1
|
|
45
|
+
});
|
|
46
|
+
c() && (d = i ? [...d, ...e.items] : e.items, f = e.total, l = t, u = i ? u + 1 : 1, p = null, a.onFetch?.({
|
|
47
|
+
durationMs: Date.now() - h,
|
|
48
|
+
query: o,
|
|
49
|
+
status: "success"
|
|
50
|
+
}));
|
|
51
|
+
} catch (t) {
|
|
52
|
+
if (s.aborted) return;
|
|
53
|
+
c() && (i || (d = []), f = i ? f : 0, p = new e(n(t), {
|
|
54
|
+
cause: t,
|
|
55
|
+
context: {
|
|
56
|
+
kind: "infinite",
|
|
57
|
+
limit: o.limit,
|
|
58
|
+
page: o.page,
|
|
59
|
+
...o.search && { search: o.search }
|
|
60
|
+
}
|
|
61
|
+
}), a.onFetch?.({
|
|
62
|
+
durationMs: Date.now() - h,
|
|
63
|
+
error: p,
|
|
64
|
+
query: o,
|
|
65
|
+
status: "error"
|
|
66
|
+
}));
|
|
67
|
+
} finally {
|
|
68
|
+
m = !1;
|
|
69
|
+
}
|
|
70
|
+
}, () => {
|
|
71
|
+
w(), y.pendingCount() === 0 && T();
|
|
72
|
+
});
|
|
73
|
+
}, D = () => E(1, !1);
|
|
74
|
+
return b && D(), y.startRefreshInterval(() => void D()), {
|
|
75
|
+
get current() {
|
|
76
|
+
return g;
|
|
77
|
+
},
|
|
78
|
+
get disposalSignal() {
|
|
79
|
+
return y.disposalSignal;
|
|
80
|
+
},
|
|
81
|
+
dispose: () => {
|
|
82
|
+
T(), y.dispose();
|
|
83
|
+
},
|
|
84
|
+
get disposed() {
|
|
85
|
+
return y.disposed;
|
|
86
|
+
},
|
|
87
|
+
loadMore() {
|
|
88
|
+
return !_.hasMore || y.pendingCount() > 0 || m ? Promise.resolve() : (m = !0, v(), E(l + 1, !0));
|
|
89
|
+
},
|
|
90
|
+
get meta() {
|
|
91
|
+
return _;
|
|
92
|
+
},
|
|
93
|
+
patch(e) {
|
|
94
|
+
let t = !1;
|
|
95
|
+
if (e.limit !== void 0) {
|
|
96
|
+
let n = Math.max(1, Math.trunc(e.limit));
|
|
97
|
+
n !== s && (s = n, t = !0);
|
|
98
|
+
}
|
|
99
|
+
return "search" in e && (e.search !== c || y.core.isScheduled) && (c = e.search ?? "", t = !0), t ? (y.core.cancelTimer(), T(), d = [], f = 0, l = 1, u = 0, D()) : Promise.resolve();
|
|
100
|
+
},
|
|
101
|
+
get query() {
|
|
102
|
+
return {
|
|
103
|
+
...c && { search: c },
|
|
104
|
+
limit: s,
|
|
105
|
+
page: l
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
ready(e) {
|
|
109
|
+
return y.ready(e);
|
|
110
|
+
},
|
|
111
|
+
reset() {
|
|
112
|
+
return y.core.cancelTimer(), T(), l = 1, u = 0, d = [], f = 0, c = "", D();
|
|
113
|
+
},
|
|
114
|
+
search(e, t) {
|
|
115
|
+
if (t?.immediate) return e === c && !y.core.isScheduled ? Promise.resolve() : (y.core.cancelTimer(), T(), c = e, d = [], f = 0, u = 0, l = 1, D());
|
|
116
|
+
if (e === c) return Promise.resolve();
|
|
117
|
+
T(), c = e, d = [], f = 0, u = 0, l = 1;
|
|
118
|
+
let n, r = new Promise((e) => {
|
|
119
|
+
n = e;
|
|
120
|
+
});
|
|
121
|
+
return h = {
|
|
122
|
+
promise: r,
|
|
123
|
+
resolve: n
|
|
124
|
+
}, y.core.schedule(() => void D(), x), w(), r;
|
|
125
|
+
},
|
|
126
|
+
subscribe: (e) => y.core.subscribe(e),
|
|
127
|
+
[Symbol.dispose]: () => {
|
|
128
|
+
T(), y.dispose();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
export { a as createInfiniteSource };
|
|
134
|
+
|
|
135
|
+
//# sourceMappingURL=infiniteSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"infiniteSource.js","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":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./errors.cjs"),t=require("./_utils.cjs"),n=require("./core.cjs"),r=require("./pagination.cjs");var i=20,a=300;function o(e,t){let n=t.toLowerCase();return e.filter(e=>JSON.stringify(e).toLowerCase().includes(n))}function s(s,c={}){let l=n.createSourceCore(),u=c.filterAsync!==void 0||c.sortAsync!==void 0,d=c.debounceMs??a,f=c.initialData??s,p=``,m=c.filter,h=c.sort,g=c.limit??i,_=1,v=[],y=!1,b=!1,x=null,S=null,C=null,w=()=>l.notify(),T=()=>{C&&=(C.resolve(),null)},E=()=>{let e=f;return p&&(e=(c.searchFn??o)(e,p)),m&&(e=e.filter(m)),h&&(e=[...e].sort(h)),e},D=()=>{v=E();let e=r.pageCount(v.length,g);_=r.clampPage(_,e)},O=async()=>{S?.abort();let n=new AbortController;S=n,y=!0,x=null,w();try{let e=f;if(p&&(e=(c.searchFn??o)(e,p)),m&&(e=e.filter(m)),c.filterAsync&&!n.signal.aborted&&(e=await c.filterAsync(e,n.signal)),h&&(e=[...e].sort(h)),c.sortAsync&&!n.signal.aborted&&(e=await c.sortAsync(e,n.signal)),n.signal.aborted)return;v=e,y=!1,b=!1;let t=r.pageCount(v.length,g);_=r.clampPage(_,t)}catch(r){n.signal.aborted||(y=!1,b=!1,x=new e.SourcererError(t.extractError(r),{cause:r}))}finally{S===n&&(S=null)}w(),T()},k=()=>u?O():(b=!1,D(),w(),T(),Promise.resolve());u?(v=E(),O()):D();let A={get current(){let e=(_-1)*g;return v.slice(e,e+g)},get disposalSignal(){return l.disposalSignal},dispose(){l.isDisposed||(l.cancelTimer(),S?.abort(),S=null,T(),l.dispose())},get disposed(){return l.isDisposed},goTo(e){let t=r.clampPage(e,r.pageCount(v.length,g));return t===_?Promise.resolve():(_=t,w(),Promise.resolve())},goToLast(){return A.goTo(r.pageCount(v.length,g))},get meta(){return r.createMeta({error:x,isLoading:y,isSearchPending:b,pageNumber:_,pageSize:g,totalItems:v.length})},next(){return A.goTo(_+1)},patch(e){let t=!1;if(e.limit!==void 0){let n=Math.max(1,Math.trunc(e.limit));n!==g&&(g=n,t=!0)}if(`filter`in e&&(m=e.filter,t=!0),`sort`in e&&(h=e.sort,t=!0),`search`in e&&(e.search!==p||l.isScheduled)&&(p=e.search??``,t=!0),e.page!==void 0){let n=r.clampPage(e.page,r.pageCount(v.length,g));n!==_&&(_=n,t=!0)}return t?(l.cancelTimer(),T(),(e.limit!==void 0||`filter`in e||`sort`in e||`search`in e)&&e.page===void 0&&(_=1),k()):Promise.resolve()},prev(){return A.goTo(_-1)},get query(){return{limit:g,page:_,...p&&{search:p}}},ready(e){return l.ready(()=>!y&&!l.isScheduled,e)},reset(){return l.cancelTimer(),b=!1,p=``,m=c.filter,h=c.sort,_=1,T(),k()},search(e,t){if(t?.immediate)return e===p&&!l.isScheduled?Promise.resolve():(l.cancelTimer(),b=!1,p=e,_=1,T(),k());if(e===p)return Promise.resolve();T(),p=e,_=1,b=!0,w();let n,r=new Promise(e=>{n=e});return C={promise:r,resolve:n},l.schedule(()=>{b=!1,k()},d),r},setData(e){return f=e,_=1,k()},subscribe(e){return l.subscribe(e)},[Symbol.dispose](){A.dispose()}};return A}exports.createLocalSource=s;
|
|
2
|
+
//# sourceMappingURL=localSource.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localSource.cjs","names":[],"sources":["../src/localSource.ts"],"sourcesContent":["import type { LocalSource, LocalSourceConfig, LocalSourceQuery, SearchOptions, SourceQuery } from './types';\n\nimport { extractError } from './_utils';\nimport { createSourceCore } from './core';\nimport { SourcererError } from './errors';\nimport { clampPage, createMeta, pageCount } from './pagination';\n\ntype PendingSearch = { promise: Promise<void>; resolve: () => void };\n\nconst DEFAULT_LIMIT = 20;\nconst DEFAULT_DEBOUNCE_MS = 300;\n\nfunction defaultSearch<T>(items: readonly T[], query: string): readonly T[] {\n const q = query.toLowerCase();\n\n return items.filter((item) => JSON.stringify(item).toLowerCase().includes(q));\n}\n\n/**\n * Creates an in-memory paginated source with optional async filter/sort pipelines.\n *\n * When neither `filterAsync` nor `sortAsync` are configured, all operations\n * complete synchronously — there is no loading state and `ready()` resolves immediately.\n * Provide async ops to offload heavy computation (e.g. to a Web Worker).\n *\n * @example\n * ```ts\n * // Synchronous (common case)\n * const source = createLocalSource([1, 2, 3, 4, 5], { limit: 2 });\n * await source.goTo(2);\n * console.log(source.current); // [3, 4]\n *\n * // Async pipeline (Web Worker offloading)\n * const source = createLocalSource(users, {\n * filterAsync: (items, signal) => myWorker.filter(items, signal),\n * sortAsync: (items, signal) => myWorker.sort(items, signal),\n * });\n * ```\n */\nexport function createLocalSource<T>(data: readonly T[], cfg: LocalSourceConfig<T> = {}): LocalSource<T> {\n const core = createSourceCore();\n const isAsync = cfg.filterAsync !== undefined || cfg.sortAsync !== undefined;\n const debounceMs = cfg.debounceMs ?? DEFAULT_DEBOUNCE_MS;\n\n let allData: readonly T[] = cfg.initialData ?? data;\n let search = '';\n let filter = cfg.filter;\n let sort = cfg.sort;\n let limit = cfg.limit ?? DEFAULT_LIMIT;\n let page = 1;\n let processed: readonly T[] = [];\n let isLoading = false;\n let isSearchPending = false;\n let error: SourcererError | null = null;\n let asyncController: AbortController | null = null;\n let pendingSearch: PendingSearch | null = null;\n\n const commit = () => core.notify();\n\n const resolvePendingSearch = () => {\n if (pendingSearch) {\n pendingSearch.resolve();\n pendingSearch = null;\n }\n };\n\n // ── Sync pipeline (used when no async ops) ──────────────────────────────────\n const runSyncPipeline = (): readonly T[] => {\n let result: readonly T[] = allData;\n\n if (search) result = (cfg.searchFn ?? defaultSearch)(result, search);\n\n if (filter) result = result.filter(filter);\n\n if (sort) result = [...result].sort(sort);\n\n return result;\n };\n\n const recompute = () => {\n processed = runSyncPipeline();\n\n const pages = pageCount(processed.length, limit);\n\n page = clampPage(page, pages);\n };\n\n // ── Async pipeline ──────────────────────────────────────────────────────────\n const runAsyncPipeline = async (): Promise<void> => {\n asyncController?.abort();\n\n const controller = new AbortController();\n\n asyncController = controller;\n isLoading = true;\n error = null;\n commit();\n\n try {\n let result: readonly T[] = allData;\n\n if (search) result = (cfg.searchFn ?? defaultSearch)(result, search);\n\n if (filter) result = result.filter(filter);\n\n if (cfg.filterAsync && !controller.signal.aborted) {\n result = await cfg.filterAsync(result, controller.signal);\n }\n\n if (sort) result = [...result].sort(sort);\n\n if (cfg.sortAsync && !controller.signal.aborted) {\n result = await cfg.sortAsync(result, controller.signal);\n }\n\n if (controller.signal.aborted) return;\n\n processed = result;\n isLoading = false;\n isSearchPending = false;\n\n const pages = pageCount(processed.length, limit);\n\n page = clampPage(page, pages);\n } catch (reason: unknown) {\n if (!controller.signal.aborted) {\n isLoading = false;\n isSearchPending = false;\n error = new SourcererError(extractError(reason), { cause: reason });\n }\n } finally {\n if (asyncController === controller) asyncController = null;\n }\n\n commit();\n resolvePendingSearch();\n };\n\n // Internal flush — not part of public API\n const flushInternal = (): Promise<void> => {\n if (isAsync) return runAsyncPipeline();\n\n isSearchPending = false;\n recompute();\n commit();\n resolvePendingSearch();\n\n return Promise.resolve();\n };\n\n // ── Initial state ───────────────────────────────────────────────────────────\n if (isAsync) {\n // Async: seed with a sync pass so `current` isn't empty while the async pass runs.\n processed = runSyncPipeline();\n void runAsyncPipeline();\n } else {\n recompute();\n }\n\n const source: LocalSource<T> = {\n get current() {\n const start = (page - 1) * limit;\n\n return processed.slice(start, start + limit);\n },\n\n get disposalSignal() {\n return core.disposalSignal;\n },\n\n dispose() {\n if (core.isDisposed) return;\n\n core.cancelTimer();\n asyncController?.abort();\n asyncController = null;\n resolvePendingSearch();\n core.dispose();\n },\n\n get disposed() {\n return core.isDisposed;\n },\n\n goTo(n) {\n const pages = pageCount(processed.length, limit);\n const clamped = clampPage(n, pages);\n\n if (clamped === page) return Promise.resolve();\n\n page = clamped;\n commit();\n\n return Promise.resolve();\n },\n\n goToLast() {\n return source.goTo(pageCount(processed.length, limit));\n },\n\n get meta() {\n return createMeta({\n error,\n isLoading,\n isSearchPending,\n pageNumber: page,\n pageSize: limit,\n totalItems: processed.length,\n });\n },\n\n next() {\n return source.goTo(page + 1);\n },\n\n patch(changes: LocalSourceQuery<T>) {\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 if ('filter' in changes) {\n filter = changes.filter;\n changed = true;\n }\n\n if ('sort' in changes) {\n sort = changes.sort;\n changed = true;\n }\n\n // `|| 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 || core.isScheduled)) {\n search = changes.search ?? '';\n changed = true;\n }\n\n if (changes.page !== undefined) {\n const next = clampPage(changes.page, pageCount(processed.length, limit));\n\n if (next !== page) {\n page = next;\n changed = true;\n }\n }\n\n if (!changed) return Promise.resolve();\n\n // patch() is documented as a single atomic recompute — cancel any debounced search()\n // still pending so it doesn't fire a second, redundant recompute afterwards.\n core.cancelTimer();\n resolvePendingSearch();\n\n // Reset page only when non-page query fields changed without an explicit page\n if (\n (changes.limit !== undefined || 'filter' in changes || 'sort' in changes || 'search' in changes) &&\n changes.page === undefined\n ) {\n page = 1;\n }\n\n return flushInternal();\n },\n\n prev() {\n return source.goTo(page - 1);\n },\n\n get query(): SourceQuery {\n return {\n limit,\n page,\n ...(search && { search }),\n };\n },\n\n ready(timeout?: number) {\n return core.ready(() => !isLoading && !core.isScheduled, timeout);\n },\n\n reset() {\n core.cancelTimer();\n isSearchPending = false;\n search = '';\n filter = cfg.filter;\n sort = cfg.sort;\n page = 1;\n resolvePendingSearch();\n\n return flushInternal();\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 recompute already happened.\n if (q === search && !core.isScheduled) return Promise.resolve();\n\n core.cancelTimer();\n isSearchPending = false;\n search = q;\n page = 1;\n resolvePendingSearch();\n\n return flushInternal();\n }\n\n if (q === search) return Promise.resolve();\n\n // Cancel any previous pending search promise\n resolvePendingSearch();\n\n search = q;\n page = 1;\n isSearchPending = true;\n commit();\n\n let resolveSearch!: () => void;\n const promise = new Promise<void>((res) => {\n resolveSearch = res;\n });\n\n pendingSearch = { promise, resolve: resolveSearch };\n\n core.schedule(() => {\n isSearchPending = false;\n void flushInternal();\n }, debounceMs);\n\n return promise;\n },\n\n setData(d) {\n allData = d;\n page = 1;\n\n return flushInternal();\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":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LocalSource, LocalSourceConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Creates an in-memory paginated source with optional async filter/sort pipelines.
|
|
4
|
+
*
|
|
5
|
+
* When neither `filterAsync` nor `sortAsync` are configured, all operations
|
|
6
|
+
* complete synchronously — there is no loading state and `ready()` resolves immediately.
|
|
7
|
+
* Provide async ops to offload heavy computation (e.g. to a Web Worker).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* // Synchronous (common case)
|
|
12
|
+
* const source = createLocalSource([1, 2, 3, 4, 5], { limit: 2 });
|
|
13
|
+
* await source.goTo(2);
|
|
14
|
+
* console.log(source.current); // [3, 4]
|
|
15
|
+
*
|
|
16
|
+
* // Async pipeline (Web Worker offloading)
|
|
17
|
+
* const source = createLocalSource(users, {
|
|
18
|
+
* filterAsync: (items, signal) => myWorker.filter(items, signal),
|
|
19
|
+
* sortAsync: (items, signal) => myWorker.sort(items, signal),
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function createLocalSource<T>(data: readonly T[], cfg?: LocalSourceConfig<T>): LocalSource<T>;
|
|
24
|
+
//# sourceMappingURL=localSource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localSource.d.ts","sourceRoot":"","sources":["../src/localSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAgD,MAAM,SAAS,CAAC;AAkB5G;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,EAAE,GAAG,GAAE,iBAAiB,CAAC,CAAC,CAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CA6TvG"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { SourcererError as e } from "./errors.js";
|
|
2
|
+
import { extractError as t } from "./_utils.js";
|
|
3
|
+
import { createSourceCore as n } from "./core.js";
|
|
4
|
+
import { clampPage as r, createMeta as i, pageCount as a } from "./pagination.js";
|
|
5
|
+
//#region src/localSource.ts
|
|
6
|
+
var o = 20, s = 300;
|
|
7
|
+
function c(e, t) {
|
|
8
|
+
let n = t.toLowerCase();
|
|
9
|
+
return e.filter((e) => JSON.stringify(e).toLowerCase().includes(n));
|
|
10
|
+
}
|
|
11
|
+
function l(l, u = {}) {
|
|
12
|
+
let d = n(), f = u.filterAsync !== void 0 || u.sortAsync !== void 0, p = u.debounceMs ?? s, m = u.initialData ?? l, h = "", g = u.filter, _ = u.sort, v = u.limit ?? o, y = 1, b = [], x = !1, S = !1, C = null, w = null, T = null, E = () => d.notify(), D = () => {
|
|
13
|
+
T &&= (T.resolve(), null);
|
|
14
|
+
}, O = () => {
|
|
15
|
+
let e = m;
|
|
16
|
+
return h && (e = (u.searchFn ?? c)(e, h)), g && (e = e.filter(g)), _ && (e = [...e].sort(_)), e;
|
|
17
|
+
}, k = () => {
|
|
18
|
+
b = O();
|
|
19
|
+
let e = a(b.length, v);
|
|
20
|
+
y = r(y, e);
|
|
21
|
+
}, A = async () => {
|
|
22
|
+
w?.abort();
|
|
23
|
+
let n = new AbortController();
|
|
24
|
+
w = n, x = !0, C = null, E();
|
|
25
|
+
try {
|
|
26
|
+
let e = m;
|
|
27
|
+
if (h && (e = (u.searchFn ?? c)(e, h)), g && (e = e.filter(g)), u.filterAsync && !n.signal.aborted && (e = await u.filterAsync(e, n.signal)), _ && (e = [...e].sort(_)), u.sortAsync && !n.signal.aborted && (e = await u.sortAsync(e, n.signal)), n.signal.aborted) return;
|
|
28
|
+
b = e, x = !1, S = !1;
|
|
29
|
+
let t = a(b.length, v);
|
|
30
|
+
y = r(y, t);
|
|
31
|
+
} catch (r) {
|
|
32
|
+
n.signal.aborted || (x = !1, S = !1, C = new e(t(r), { cause: r }));
|
|
33
|
+
} finally {
|
|
34
|
+
w === n && (w = null);
|
|
35
|
+
}
|
|
36
|
+
E(), D();
|
|
37
|
+
}, j = () => f ? A() : (S = !1, k(), E(), D(), Promise.resolve());
|
|
38
|
+
f ? (b = O(), A()) : k();
|
|
39
|
+
let M = {
|
|
40
|
+
get current() {
|
|
41
|
+
let e = (y - 1) * v;
|
|
42
|
+
return b.slice(e, e + v);
|
|
43
|
+
},
|
|
44
|
+
get disposalSignal() {
|
|
45
|
+
return d.disposalSignal;
|
|
46
|
+
},
|
|
47
|
+
dispose() {
|
|
48
|
+
d.isDisposed || (d.cancelTimer(), w?.abort(), w = null, D(), d.dispose());
|
|
49
|
+
},
|
|
50
|
+
get disposed() {
|
|
51
|
+
return d.isDisposed;
|
|
52
|
+
},
|
|
53
|
+
goTo(e) {
|
|
54
|
+
let t = r(e, a(b.length, v));
|
|
55
|
+
return t === y ? Promise.resolve() : (y = t, E(), Promise.resolve());
|
|
56
|
+
},
|
|
57
|
+
goToLast() {
|
|
58
|
+
return M.goTo(a(b.length, v));
|
|
59
|
+
},
|
|
60
|
+
get meta() {
|
|
61
|
+
return i({
|
|
62
|
+
error: C,
|
|
63
|
+
isLoading: x,
|
|
64
|
+
isSearchPending: S,
|
|
65
|
+
pageNumber: y,
|
|
66
|
+
pageSize: v,
|
|
67
|
+
totalItems: b.length
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
next() {
|
|
71
|
+
return M.goTo(y + 1);
|
|
72
|
+
},
|
|
73
|
+
patch(e) {
|
|
74
|
+
let t = !1;
|
|
75
|
+
if (e.limit !== void 0) {
|
|
76
|
+
let n = Math.max(1, Math.trunc(e.limit));
|
|
77
|
+
n !== v && (v = n, t = !0);
|
|
78
|
+
}
|
|
79
|
+
if ("filter" in e && (g = e.filter, t = !0), "sort" in e && (_ = e.sort, t = !0), "search" in e && (e.search !== h || d.isScheduled) && (h = e.search ?? "", t = !0), e.page !== void 0) {
|
|
80
|
+
let n = r(e.page, a(b.length, v));
|
|
81
|
+
n !== y && (y = n, t = !0);
|
|
82
|
+
}
|
|
83
|
+
return t ? (d.cancelTimer(), D(), (e.limit !== void 0 || "filter" in e || "sort" in e || "search" in e) && e.page === void 0 && (y = 1), j()) : Promise.resolve();
|
|
84
|
+
},
|
|
85
|
+
prev() {
|
|
86
|
+
return M.goTo(y - 1);
|
|
87
|
+
},
|
|
88
|
+
get query() {
|
|
89
|
+
return {
|
|
90
|
+
limit: v,
|
|
91
|
+
page: y,
|
|
92
|
+
...h && { search: h }
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
ready(e) {
|
|
96
|
+
return d.ready(() => !x && !d.isScheduled, e);
|
|
97
|
+
},
|
|
98
|
+
reset() {
|
|
99
|
+
return d.cancelTimer(), S = !1, h = "", g = u.filter, _ = u.sort, y = 1, D(), j();
|
|
100
|
+
},
|
|
101
|
+
search(e, t) {
|
|
102
|
+
if (t?.immediate) return e === h && !d.isScheduled ? Promise.resolve() : (d.cancelTimer(), S = !1, h = e, y = 1, D(), j());
|
|
103
|
+
if (e === h) return Promise.resolve();
|
|
104
|
+
D(), h = e, y = 1, S = !0, E();
|
|
105
|
+
let n, r = new Promise((e) => {
|
|
106
|
+
n = e;
|
|
107
|
+
});
|
|
108
|
+
return T = {
|
|
109
|
+
promise: r,
|
|
110
|
+
resolve: n
|
|
111
|
+
}, d.schedule(() => {
|
|
112
|
+
S = !1, j();
|
|
113
|
+
}, p), r;
|
|
114
|
+
},
|
|
115
|
+
setData(e) {
|
|
116
|
+
return m = e, y = 1, j();
|
|
117
|
+
},
|
|
118
|
+
subscribe(e) {
|
|
119
|
+
return d.subscribe(e);
|
|
120
|
+
},
|
|
121
|
+
[Symbol.dispose]() {
|
|
122
|
+
M.dispose();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
return M;
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { l as createLocalSource };
|
|
129
|
+
|
|
130
|
+
//# sourceMappingURL=localSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localSource.js","names":[],"sources":["../src/localSource.ts"],"sourcesContent":["import type { LocalSource, LocalSourceConfig, LocalSourceQuery, SearchOptions, SourceQuery } from './types';\n\nimport { extractError } from './_utils';\nimport { createSourceCore } from './core';\nimport { SourcererError } from './errors';\nimport { clampPage, createMeta, pageCount } from './pagination';\n\ntype PendingSearch = { promise: Promise<void>; resolve: () => void };\n\nconst DEFAULT_LIMIT = 20;\nconst DEFAULT_DEBOUNCE_MS = 300;\n\nfunction defaultSearch<T>(items: readonly T[], query: string): readonly T[] {\n const q = query.toLowerCase();\n\n return items.filter((item) => JSON.stringify(item).toLowerCase().includes(q));\n}\n\n/**\n * Creates an in-memory paginated source with optional async filter/sort pipelines.\n *\n * When neither `filterAsync` nor `sortAsync` are configured, all operations\n * complete synchronously — there is no loading state and `ready()` resolves immediately.\n * Provide async ops to offload heavy computation (e.g. to a Web Worker).\n *\n * @example\n * ```ts\n * // Synchronous (common case)\n * const source = createLocalSource([1, 2, 3, 4, 5], { limit: 2 });\n * await source.goTo(2);\n * console.log(source.current); // [3, 4]\n *\n * // Async pipeline (Web Worker offloading)\n * const source = createLocalSource(users, {\n * filterAsync: (items, signal) => myWorker.filter(items, signal),\n * sortAsync: (items, signal) => myWorker.sort(items, signal),\n * });\n * ```\n */\nexport function createLocalSource<T>(data: readonly T[], cfg: LocalSourceConfig<T> = {}): LocalSource<T> {\n const core = createSourceCore();\n const isAsync = cfg.filterAsync !== undefined || cfg.sortAsync !== undefined;\n const debounceMs = cfg.debounceMs ?? DEFAULT_DEBOUNCE_MS;\n\n let allData: readonly T[] = cfg.initialData ?? data;\n let search = '';\n let filter = cfg.filter;\n let sort = cfg.sort;\n let limit = cfg.limit ?? DEFAULT_LIMIT;\n let page = 1;\n let processed: readonly T[] = [];\n let isLoading = false;\n let isSearchPending = false;\n let error: SourcererError | null = null;\n let asyncController: AbortController | null = null;\n let pendingSearch: PendingSearch | null = null;\n\n const commit = () => core.notify();\n\n const resolvePendingSearch = () => {\n if (pendingSearch) {\n pendingSearch.resolve();\n pendingSearch = null;\n }\n };\n\n // ── Sync pipeline (used when no async ops) ──────────────────────────────────\n const runSyncPipeline = (): readonly T[] => {\n let result: readonly T[] = allData;\n\n if (search) result = (cfg.searchFn ?? defaultSearch)(result, search);\n\n if (filter) result = result.filter(filter);\n\n if (sort) result = [...result].sort(sort);\n\n return result;\n };\n\n const recompute = () => {\n processed = runSyncPipeline();\n\n const pages = pageCount(processed.length, limit);\n\n page = clampPage(page, pages);\n };\n\n // ── Async pipeline ──────────────────────────────────────────────────────────\n const runAsyncPipeline = async (): Promise<void> => {\n asyncController?.abort();\n\n const controller = new AbortController();\n\n asyncController = controller;\n isLoading = true;\n error = null;\n commit();\n\n try {\n let result: readonly T[] = allData;\n\n if (search) result = (cfg.searchFn ?? defaultSearch)(result, search);\n\n if (filter) result = result.filter(filter);\n\n if (cfg.filterAsync && !controller.signal.aborted) {\n result = await cfg.filterAsync(result, controller.signal);\n }\n\n if (sort) result = [...result].sort(sort);\n\n if (cfg.sortAsync && !controller.signal.aborted) {\n result = await cfg.sortAsync(result, controller.signal);\n }\n\n if (controller.signal.aborted) return;\n\n processed = result;\n isLoading = false;\n isSearchPending = false;\n\n const pages = pageCount(processed.length, limit);\n\n page = clampPage(page, pages);\n } catch (reason: unknown) {\n if (!controller.signal.aborted) {\n isLoading = false;\n isSearchPending = false;\n error = new SourcererError(extractError(reason), { cause: reason });\n }\n } finally {\n if (asyncController === controller) asyncController = null;\n }\n\n commit();\n resolvePendingSearch();\n };\n\n // Internal flush — not part of public API\n const flushInternal = (): Promise<void> => {\n if (isAsync) return runAsyncPipeline();\n\n isSearchPending = false;\n recompute();\n commit();\n resolvePendingSearch();\n\n return Promise.resolve();\n };\n\n // ── Initial state ───────────────────────────────────────────────────────────\n if (isAsync) {\n // Async: seed with a sync pass so `current` isn't empty while the async pass runs.\n processed = runSyncPipeline();\n void runAsyncPipeline();\n } else {\n recompute();\n }\n\n const source: LocalSource<T> = {\n get current() {\n const start = (page - 1) * limit;\n\n return processed.slice(start, start + limit);\n },\n\n get disposalSignal() {\n return core.disposalSignal;\n },\n\n dispose() {\n if (core.isDisposed) return;\n\n core.cancelTimer();\n asyncController?.abort();\n asyncController = null;\n resolvePendingSearch();\n core.dispose();\n },\n\n get disposed() {\n return core.isDisposed;\n },\n\n goTo(n) {\n const pages = pageCount(processed.length, limit);\n const clamped = clampPage(n, pages);\n\n if (clamped === page) return Promise.resolve();\n\n page = clamped;\n commit();\n\n return Promise.resolve();\n },\n\n goToLast() {\n return source.goTo(pageCount(processed.length, limit));\n },\n\n get meta() {\n return createMeta({\n error,\n isLoading,\n isSearchPending,\n pageNumber: page,\n pageSize: limit,\n totalItems: processed.length,\n });\n },\n\n next() {\n return source.goTo(page + 1);\n },\n\n patch(changes: LocalSourceQuery<T>) {\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 if ('filter' in changes) {\n filter = changes.filter;\n changed = true;\n }\n\n if ('sort' in changes) {\n sort = changes.sort;\n changed = true;\n }\n\n // `|| 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 || core.isScheduled)) {\n search = changes.search ?? '';\n changed = true;\n }\n\n if (changes.page !== undefined) {\n const next = clampPage(changes.page, pageCount(processed.length, limit));\n\n if (next !== page) {\n page = next;\n changed = true;\n }\n }\n\n if (!changed) return Promise.resolve();\n\n // patch() is documented as a single atomic recompute — cancel any debounced search()\n // still pending so it doesn't fire a second, redundant recompute afterwards.\n core.cancelTimer();\n resolvePendingSearch();\n\n // Reset page only when non-page query fields changed without an explicit page\n if (\n (changes.limit !== undefined || 'filter' in changes || 'sort' in changes || 'search' in changes) &&\n changes.page === undefined\n ) {\n page = 1;\n }\n\n return flushInternal();\n },\n\n prev() {\n return source.goTo(page - 1);\n },\n\n get query(): SourceQuery {\n return {\n limit,\n page,\n ...(search && { search }),\n };\n },\n\n ready(timeout?: number) {\n return core.ready(() => !isLoading && !core.isScheduled, timeout);\n },\n\n reset() {\n core.cancelTimer();\n isSearchPending = false;\n search = '';\n filter = cfg.filter;\n sort = cfg.sort;\n page = 1;\n resolvePendingSearch();\n\n return flushInternal();\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 recompute already happened.\n if (q === search && !core.isScheduled) return Promise.resolve();\n\n core.cancelTimer();\n isSearchPending = false;\n search = q;\n page = 1;\n resolvePendingSearch();\n\n return flushInternal();\n }\n\n if (q === search) return Promise.resolve();\n\n // Cancel any previous pending search promise\n resolvePendingSearch();\n\n search = q;\n page = 1;\n isSearchPending = true;\n commit();\n\n let resolveSearch!: () => void;\n const promise = new Promise<void>((res) => {\n resolveSearch = res;\n });\n\n pendingSearch = { promise, resolve: resolveSearch };\n\n core.schedule(() => {\n isSearchPending = false;\n void flushInternal();\n }, debounceMs);\n\n return promise;\n },\n\n setData(d) {\n allData = d;\n page = 1;\n\n return flushInternal();\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":"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"}
|
package/dist/merge.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./core.cjs");function t(t,n){let r=e.createSourceCore(),i=n(t.map(e=>e.current)),a=t.map(e=>e.subscribe(()=>{i=n(t.map(e=>e.current)),r.notify()})),o={get current(){return i},get disposalSignal(){return r.disposalSignal},dispose(){if(!r.isDisposed){for(let e of a)e();r.dispose()}},get disposed(){return r.isDisposed},subscribe(e){return r.subscribe(e)},[Symbol.dispose](){o.dispose()}};return o}exports.mergeSource=t;
|
|
2
|
+
//# sourceMappingURL=merge.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.cjs","names":[],"sources":["../src/merge.ts"],"sourcesContent":["import type { MergedSource } from './types';\n\nimport { createSourceCore } from './core';\n\n/**\n * Combines multiple reactive sources into a single read-only view by applying\n * a user-supplied `combine` function over all sources' `current` arrays.\n *\n * The merged source recalculates whenever any parent source changes. Useful for\n * offline/online hybrid UIs (local + remote merged), multi-dataset views, or\n * pagination across multiple backends.\n *\n * Call `dispose()` to unsubscribe from all parent sources.\n *\n * @example\n * ```ts\n * const local = createLocalSource(localCache, { limit: 50 });\n * const remote = createRemoteSource({ fetch: fetchUsers, limit: 50 });\n *\n * // Show local cache items first, then remote items (deduped by id).\n * const merged = mergeSource([local, remote], (all) => {\n * const seen = new Set<number>();\n * return all.flat().filter((u) => !seen.has(u.id) && seen.add(u.id));\n * });\n *\n * merged.subscribe(() => console.log(merged.current));\n * ```\n */\nexport function mergeSource<T>(\n sources: ReadonlyArray<{\n readonly current: readonly T[];\n subscribe(listener: () => void): () => void;\n }>,\n combine: (allItems: ReadonlyArray<readonly T[]>) => readonly T[],\n): MergedSource<T> {\n const core = createSourceCore();\n let cachedCurrent: readonly T[] = combine(sources.map((s) => s.current));\n\n const unsubscribeFns = sources.map((source) =>\n source.subscribe(() => {\n cachedCurrent = combine(sources.map((s) => s.current));\n core.notify();\n }),\n );\n\n const source: MergedSource<T> = {\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 for (const unsub of unsubscribeFns) {\n unsub();\n }\n\n core.dispose();\n },\n\n get disposed() {\n return core.isDisposed;\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":"8BA4BA,SAAgB,EACd,EAIA,EACiB,CACjB,IAAM,EAAO,EAAA,iBAAiB,EAC1B,EAA8B,EAAQ,EAAQ,IAAK,GAAM,EAAE,OAAO,CAAC,EAEjE,EAAiB,EAAQ,IAAK,GAClC,EAAO,cAAgB,CACrB,EAAgB,EAAQ,EAAQ,IAAK,GAAM,EAAE,OAAO,CAAC,EACrD,EAAK,OAAO,CACd,CAAC,CACH,EAEM,EAA0B,CAC9B,IAAI,SAAU,CACZ,OAAO,CACT,EAEA,IAAI,gBAAiB,CACnB,OAAO,EAAK,cACd,EAEA,SAAU,CACJ,MAAK,WAET,KAAK,IAAM,KAAS,EAClB,EAAM,EAGR,EAAK,QAAQ,CAHL,CAIV,EAEA,IAAI,UAAW,CACb,OAAO,EAAK,UACd,EAEA,UAAU,EAAU,CAClB,OAAO,EAAK,UAAU,CAAQ,CAChC,EAEA,CAAC,OAAO,UAAW,CACjB,EAAO,QAAQ,CACjB,CACF,EAEA,OAAO,CACT"}
|
package/dist/merge.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MergedSource } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Combines multiple reactive sources into a single read-only view by applying
|
|
4
|
+
* a user-supplied `combine` function over all sources' `current` arrays.
|
|
5
|
+
*
|
|
6
|
+
* The merged source recalculates whenever any parent source changes. Useful for
|
|
7
|
+
* offline/online hybrid UIs (local + remote merged), multi-dataset views, or
|
|
8
|
+
* pagination across multiple backends.
|
|
9
|
+
*
|
|
10
|
+
* Call `dispose()` to unsubscribe from all parent sources.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const local = createLocalSource(localCache, { limit: 50 });
|
|
15
|
+
* const remote = createRemoteSource({ fetch: fetchUsers, limit: 50 });
|
|
16
|
+
*
|
|
17
|
+
* // Show local cache items first, then remote items (deduped by id).
|
|
18
|
+
* const merged = mergeSource([local, remote], (all) => {
|
|
19
|
+
* const seen = new Set<number>();
|
|
20
|
+
* return all.flat().filter((u) => !seen.has(u.id) && seen.add(u.id));
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* merged.subscribe(() => console.log(merged.current));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function mergeSource<T>(sources: ReadonlyArray<{
|
|
27
|
+
readonly current: readonly T[];
|
|
28
|
+
subscribe(listener: () => void): () => void;
|
|
29
|
+
}>, combine: (allItems: ReadonlyArray<readonly T[]>) => readonly T[]): MergedSource<T>;
|
|
30
|
+
//# sourceMappingURL=merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAI5C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,OAAO,EAAE,aAAa,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IAC/B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC7C,CAAC,EACF,OAAO,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,SAAS,CAAC,EAAE,GAC/D,YAAY,CAAC,CAAC,CAAC,CA4CjB"}
|
package/dist/merge.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createSourceCore as e } from "./core.js";
|
|
2
|
+
//#region src/merge.ts
|
|
3
|
+
function t(t, n) {
|
|
4
|
+
let r = e(), i = n(t.map((e) => e.current)), a = t.map((e) => e.subscribe(() => {
|
|
5
|
+
i = n(t.map((e) => e.current)), r.notify();
|
|
6
|
+
})), o = {
|
|
7
|
+
get current() {
|
|
8
|
+
return i;
|
|
9
|
+
},
|
|
10
|
+
get disposalSignal() {
|
|
11
|
+
return r.disposalSignal;
|
|
12
|
+
},
|
|
13
|
+
dispose() {
|
|
14
|
+
if (!r.isDisposed) {
|
|
15
|
+
for (let e of a) e();
|
|
16
|
+
r.dispose();
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
get disposed() {
|
|
20
|
+
return r.isDisposed;
|
|
21
|
+
},
|
|
22
|
+
subscribe(e) {
|
|
23
|
+
return r.subscribe(e);
|
|
24
|
+
},
|
|
25
|
+
[Symbol.dispose]() {
|
|
26
|
+
o.dispose();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
return o;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { t as mergeSource };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.js","names":[],"sources":["../src/merge.ts"],"sourcesContent":["import type { MergedSource } from './types';\n\nimport { createSourceCore } from './core';\n\n/**\n * Combines multiple reactive sources into a single read-only view by applying\n * a user-supplied `combine` function over all sources' `current` arrays.\n *\n * The merged source recalculates whenever any parent source changes. Useful for\n * offline/online hybrid UIs (local + remote merged), multi-dataset views, or\n * pagination across multiple backends.\n *\n * Call `dispose()` to unsubscribe from all parent sources.\n *\n * @example\n * ```ts\n * const local = createLocalSource(localCache, { limit: 50 });\n * const remote = createRemoteSource({ fetch: fetchUsers, limit: 50 });\n *\n * // Show local cache items first, then remote items (deduped by id).\n * const merged = mergeSource([local, remote], (all) => {\n * const seen = new Set<number>();\n * return all.flat().filter((u) => !seen.has(u.id) && seen.add(u.id));\n * });\n *\n * merged.subscribe(() => console.log(merged.current));\n * ```\n */\nexport function mergeSource<T>(\n sources: ReadonlyArray<{\n readonly current: readonly T[];\n subscribe(listener: () => void): () => void;\n }>,\n combine: (allItems: ReadonlyArray<readonly T[]>) => readonly T[],\n): MergedSource<T> {\n const core = createSourceCore();\n let cachedCurrent: readonly T[] = combine(sources.map((s) => s.current));\n\n const unsubscribeFns = sources.map((source) =>\n source.subscribe(() => {\n cachedCurrent = combine(sources.map((s) => s.current));\n core.notify();\n }),\n );\n\n const source: MergedSource<T> = {\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 for (const unsub of unsubscribeFns) {\n unsub();\n }\n\n core.dispose();\n },\n\n get disposed() {\n return core.isDisposed;\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":";;AA4BA,SAAgB,EACd,GAIA,GACiB;CACjB,IAAM,IAAO,EAAiB,GAC1B,IAA8B,EAAQ,EAAQ,KAAK,MAAM,EAAE,OAAO,CAAC,GAEjE,IAAiB,EAAQ,KAAK,MAClC,EAAO,gBAAgB;EAErB,AADA,IAAgB,EAAQ,EAAQ,KAAK,MAAM,EAAE,OAAO,CAAC,GACrD,EAAK,OAAO;CACd,CAAC,CACH,GAEM,IAA0B;EAC9B,IAAI,UAAU;GACZ,OAAO;EACT;EAEA,IAAI,iBAAiB;GACnB,OAAO,EAAK;EACd;EAEA,UAAU;GACJ,OAAK,YAET;SAAK,IAAM,KAAS,GAClB,EAAM;IAGR,EAAK,QAAQ;GAHL;EAIV;EAEA,IAAI,WAAW;GACb,OAAO,EAAK;EACd;EAEA,UAAU,GAAU;GAClB,OAAO,EAAK,UAAU,CAAQ;EAChC;EAEA,CAAC,OAAO,WAAW;GACjB,EAAO,QAAQ;EACjB;CACF;CAEA,OAAO;AACT"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=(e,t)=>{let n=Math.trunc(e);return Number.isFinite(n)?Math.max(t,n):t},t=(t,n)=>{let r=e(t,0),i=e(n,1);return Math.max(1,Math.ceil(r/i))},n=(t,n)=>Math.max(1,Math.min(e(t,1),Math.max(1,n))),r=r=>{let i=e(r.pageSize,1),a=e(r.totalItems,0),o=t(a,i),s=n(r.pageNumber,o);return{error:r.error,isLoading:r.isLoading,isSearchPending:r.isSearchPending,pageCount:o,pageNumber:s,pageSize:i,totalItems:a}};function i(e){if(e.totalItems===0)return{end:0,start:0};let t=(e.pageNumber-1)*e.pageSize+1;return{end:Math.min(e.pageNumber*e.pageSize,e.totalItems),start:t}}exports.clampPage=n,exports.createMeta=r,exports.itemRange=i,exports.pageCount=t;
|
|
2
|
+
//# sourceMappingURL=pagination.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.cjs","names":[],"sources":["../src/pagination.ts"],"sourcesContent":["import type { SourcererError, SourceMeta } from './types';\n\nconst clampInt = (value: number, minimum: number) => {\n const parsed = Math.trunc(value);\n\n if (!Number.isFinite(parsed)) {\n return minimum;\n }\n\n return Math.max(minimum, parsed);\n};\n\nexport const pageCount = (total: number, limit: number) => {\n const safeTotal = clampInt(total, 0);\n const safeLimit = clampInt(limit, 1);\n\n return Math.max(1, Math.ceil(safeTotal / safeLimit));\n};\n\nexport const clampPage = (page: number, pages: number) => {\n return Math.max(1, Math.min(clampInt(page, 1), Math.max(1, pages)));\n};\n\nexport const createMeta = (state: {\n error: SourcererError | null;\n isLoading: boolean;\n isSearchPending: boolean;\n pageNumber: number;\n pageSize: number;\n totalItems: number;\n}): SourceMeta => {\n const safeLimit = clampInt(state.pageSize, 1);\n const safeTotal = clampInt(state.totalItems, 0);\n const pages = pageCount(safeTotal, safeLimit);\n const page = clampPage(state.pageNumber, pages);\n\n return {\n error: state.error,\n isLoading: state.isLoading,\n isSearchPending: state.isSearchPending,\n pageCount: pages,\n pageNumber: page,\n pageSize: safeLimit,\n totalItems: safeTotal,\n };\n};\n\n/**\n * Computes the 1-based display range of items on the current page.\n * Returns `{ start: 0, end: 0 }` when there are no items.\n *\n * @example\n * ```ts\n * const { start, end } = itemRange(source.meta);\n * // \"Showing 11–20 of 50\"\n * console.log(`Showing ${start}–${end} of ${source.meta.totalItems}`);\n * ```\n */\nexport function itemRange(meta: Readonly<{ pageNumber: number; pageSize: number; totalItems: number }>): {\n end: number;\n start: number;\n} {\n if (meta.totalItems === 0) return { end: 0, start: 0 };\n\n const start = (meta.pageNumber - 1) * meta.pageSize + 1;\n\n return {\n end: Math.min(meta.pageNumber * meta.pageSize, meta.totalItems),\n start,\n };\n}\n"],"mappings":"AAEA,IAAM,GAAY,EAAe,IAAoB,CACnD,IAAM,EAAS,KAAK,MAAM,CAAK,EAM/B,OAJK,OAAO,SAAS,CAAM,EAIpB,KAAK,IAAI,EAAS,CAAM,EAHtB,CAIX,EAEa,GAAa,EAAe,IAAkB,CACzD,IAAM,EAAY,EAAS,EAAO,CAAC,EAC7B,EAAY,EAAS,EAAO,CAAC,EAEnC,OAAO,KAAK,IAAI,EAAG,KAAK,KAAK,EAAY,CAAS,CAAC,CACrD,EAEa,GAAa,EAAc,IAC/B,KAAK,IAAI,EAAG,KAAK,IAAI,EAAS,EAAM,CAAC,EAAG,KAAK,IAAI,EAAG,CAAK,CAAC,CAAC,EAGvD,EAAc,GAOT,CAChB,IAAM,EAAY,EAAS,EAAM,SAAU,CAAC,EACtC,EAAY,EAAS,EAAM,WAAY,CAAC,EACxC,EAAQ,EAAU,EAAW,CAAS,EACtC,EAAO,EAAU,EAAM,WAAY,CAAK,EAE9C,MAAO,CACL,MAAO,EAAM,MACb,UAAW,EAAM,UACjB,gBAAiB,EAAM,gBACvB,UAAW,EACX,WAAY,EACZ,SAAU,EACV,WAAY,CACd,CACF,EAaA,SAAgB,EAAU,EAGxB,CACA,GAAI,EAAK,aAAe,EAAG,MAAO,CAAE,IAAK,EAAG,MAAO,CAAE,EAErD,IAAM,GAAS,EAAK,WAAa,GAAK,EAAK,SAAW,EAEtD,MAAO,CACL,IAAK,KAAK,IAAI,EAAK,WAAa,EAAK,SAAU,EAAK,UAAU,EAC9D,OACF,CACF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SourcererError, SourceMeta } from './types';
|
|
2
|
+
export declare const pageCount: (total: number, limit: number) => number;
|
|
3
|
+
export declare const clampPage: (page: number, pages: number) => number;
|
|
4
|
+
export declare const createMeta: (state: {
|
|
5
|
+
error: SourcererError | null;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
isSearchPending: boolean;
|
|
8
|
+
pageNumber: number;
|
|
9
|
+
pageSize: number;
|
|
10
|
+
totalItems: number;
|
|
11
|
+
}) => SourceMeta;
|
|
12
|
+
/**
|
|
13
|
+
* Computes the 1-based display range of items on the current page.
|
|
14
|
+
* Returns `{ start: 0, end: 0 }` when there are no items.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const { start, end } = itemRange(source.meta);
|
|
19
|
+
* // "Showing 11–20 of 50"
|
|
20
|
+
* console.log(`Showing ${start}–${end} of ${source.meta.totalItems}`);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function itemRange(meta: Readonly<{
|
|
24
|
+
pageNumber: number;
|
|
25
|
+
pageSize: number;
|
|
26
|
+
totalItems: number;
|
|
27
|
+
}>): {
|
|
28
|
+
end: number;
|
|
29
|
+
start: number;
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=pagination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAY1D,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,EAAE,OAAO,MAAM,WAKrD,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,WAEpD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,OAAO;IAChC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB,KAAG,UAeH,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG;IACvG,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf,CASA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/pagination.ts
|
|
2
|
+
var e = (e, t) => {
|
|
3
|
+
let n = Math.trunc(e);
|
|
4
|
+
return Number.isFinite(n) ? Math.max(t, n) : t;
|
|
5
|
+
}, t = (t, n) => {
|
|
6
|
+
let r = e(t, 0), i = e(n, 1);
|
|
7
|
+
return Math.max(1, Math.ceil(r / i));
|
|
8
|
+
}, n = (t, n) => Math.max(1, Math.min(e(t, 1), Math.max(1, n))), r = (r) => {
|
|
9
|
+
let i = e(r.pageSize, 1), a = e(r.totalItems, 0), o = t(a, i), s = n(r.pageNumber, o);
|
|
10
|
+
return {
|
|
11
|
+
error: r.error,
|
|
12
|
+
isLoading: r.isLoading,
|
|
13
|
+
isSearchPending: r.isSearchPending,
|
|
14
|
+
pageCount: o,
|
|
15
|
+
pageNumber: s,
|
|
16
|
+
pageSize: i,
|
|
17
|
+
totalItems: a
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
function i(e) {
|
|
21
|
+
if (e.totalItems === 0) return {
|
|
22
|
+
end: 0,
|
|
23
|
+
start: 0
|
|
24
|
+
};
|
|
25
|
+
let t = (e.pageNumber - 1) * e.pageSize + 1;
|
|
26
|
+
return {
|
|
27
|
+
end: Math.min(e.pageNumber * e.pageSize, e.totalItems),
|
|
28
|
+
start: t
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { n as clampPage, r as createMeta, i as itemRange, t as pageCount };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=pagination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.js","names":[],"sources":["../src/pagination.ts"],"sourcesContent":["import type { SourcererError, SourceMeta } from './types';\n\nconst clampInt = (value: number, minimum: number) => {\n const parsed = Math.trunc(value);\n\n if (!Number.isFinite(parsed)) {\n return minimum;\n }\n\n return Math.max(minimum, parsed);\n};\n\nexport const pageCount = (total: number, limit: number) => {\n const safeTotal = clampInt(total, 0);\n const safeLimit = clampInt(limit, 1);\n\n return Math.max(1, Math.ceil(safeTotal / safeLimit));\n};\n\nexport const clampPage = (page: number, pages: number) => {\n return Math.max(1, Math.min(clampInt(page, 1), Math.max(1, pages)));\n};\n\nexport const createMeta = (state: {\n error: SourcererError | null;\n isLoading: boolean;\n isSearchPending: boolean;\n pageNumber: number;\n pageSize: number;\n totalItems: number;\n}): SourceMeta => {\n const safeLimit = clampInt(state.pageSize, 1);\n const safeTotal = clampInt(state.totalItems, 0);\n const pages = pageCount(safeTotal, safeLimit);\n const page = clampPage(state.pageNumber, pages);\n\n return {\n error: state.error,\n isLoading: state.isLoading,\n isSearchPending: state.isSearchPending,\n pageCount: pages,\n pageNumber: page,\n pageSize: safeLimit,\n totalItems: safeTotal,\n };\n};\n\n/**\n * Computes the 1-based display range of items on the current page.\n * Returns `{ start: 0, end: 0 }` when there are no items.\n *\n * @example\n * ```ts\n * const { start, end } = itemRange(source.meta);\n * // \"Showing 11–20 of 50\"\n * console.log(`Showing ${start}–${end} of ${source.meta.totalItems}`);\n * ```\n */\nexport function itemRange(meta: Readonly<{ pageNumber: number; pageSize: number; totalItems: number }>): {\n end: number;\n start: number;\n} {\n if (meta.totalItems === 0) return { end: 0, start: 0 };\n\n const start = (meta.pageNumber - 1) * meta.pageSize + 1;\n\n return {\n end: Math.min(meta.pageNumber * meta.pageSize, meta.totalItems),\n start,\n };\n}\n"],"mappings":";AAEA,IAAM,KAAY,GAAe,MAAoB;CACnD,IAAM,IAAS,KAAK,MAAM,CAAK;CAM/B,OAJK,OAAO,SAAS,CAAM,IAIpB,KAAK,IAAI,GAAS,CAAM,IAHtB;AAIX,GAEa,KAAa,GAAe,MAAkB;CACzD,IAAM,IAAY,EAAS,GAAO,CAAC,GAC7B,IAAY,EAAS,GAAO,CAAC;CAEnC,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,IAAY,CAAS,CAAC;AACrD,GAEa,KAAa,GAAc,MAC/B,KAAK,IAAI,GAAG,KAAK,IAAI,EAAS,GAAM,CAAC,GAAG,KAAK,IAAI,GAAG,CAAK,CAAC,CAAC,GAGvD,KAAc,MAOT;CAChB,IAAM,IAAY,EAAS,EAAM,UAAU,CAAC,GACtC,IAAY,EAAS,EAAM,YAAY,CAAC,GACxC,IAAQ,EAAU,GAAW,CAAS,GACtC,IAAO,EAAU,EAAM,YAAY,CAAK;CAE9C,OAAO;EACL,OAAO,EAAM;EACb,WAAW,EAAM;EACjB,iBAAiB,EAAM;EACvB,WAAW;EACX,YAAY;EACZ,UAAU;EACV,YAAY;CACd;AACF;AAaA,SAAgB,EAAU,GAGxB;CACA,IAAI,EAAK,eAAe,GAAG,OAAO;EAAE,KAAK;EAAG,OAAO;CAAE;CAErD,IAAM,KAAS,EAAK,aAAa,KAAK,EAAK,WAAW;CAEtD,OAAO;EACL,KAAK,KAAK,IAAI,EAAK,aAAa,EAAK,UAAU,EAAK,UAAU;EAC9D;CACF;AACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./remoteSource.cjs");var t=e=>{let t=e.query;return{items:e.current,...t.search&&{search:t.search},page:e.meta.pageNumber,total:e.meta.totalItems}};async function n(n){let r=e.createRemoteSource({...n,autoFetch:!1});try{if(await r.refresh(),r.meta.error)throw r.meta.error;return t(r)}finally{r.dispose()}}async function r(n){let r=e.createRemoteSource({...n,autoFetch:!1});if(await r.refresh(),r.meta.error)throw r.dispose(),r.meta.error;return{snapshot:t(r),source:r}}exports.prefetchSource=n,exports.prefetchSourceAndKeep=r;
|
|
2
|
+
//# sourceMappingURL=prefetch.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefetch.cjs","names":[],"sources":["../src/prefetch.ts"],"sourcesContent":["import type { RemoteConfig, RemoteSource, SourceSnapshot } from './types';\n\nimport { createRemoteSource } from './remoteSource';\n\ntype PrefetchConfig<T, TFilter, TSort> = Omit<RemoteConfig<T, TFilter, TSort>, 'autoFetch' | 'refreshInterval'>;\n\nconst buildSnapshot = <T, TFilter, TSort>(source: RemoteSource<T, TFilter, TSort>): SourceSnapshot<T> => {\n const q = source.query;\n\n return {\n items: source.current,\n ...(q.search && { search: q.search }),\n page: source.meta.pageNumber,\n total: source.meta.totalItems,\n };\n};\n\n/**\n * Fetches a single page of data and returns a serialisable `SourceSnapshot`.\n * The underlying source is created, fetched, then immediately disposed.\n * Use for SSR / static-generation where you only need the data, not a live source.\n *\n * @example\n * ```ts\n * const snapshot = await prefetchSource({ fetch: fetchUsers, limit: 20 });\n * ```\n */\nexport async function prefetchSource<T, TFilter = unknown, TSort = unknown>(\n cfg: PrefetchConfig<T, TFilter, TSort>,\n): Promise<SourceSnapshot<T>> {\n const source = createRemoteSource<T, TFilter, TSort>({ ...cfg, autoFetch: false });\n\n try {\n await source.refresh();\n\n if (source.meta.error) throw source.meta.error;\n\n return buildSnapshot(source);\n } finally {\n source.dispose();\n }\n}\n\n/**\n * Fetches a single page of data and returns both a serialisable `SourceSnapshot` and the\n * still-live `RemoteSource`. Useful when you need the snapshot for SSR serialisation\n * **and** the live source for subsequent client-side updates without a double-fetch.\n *\n * **The caller is responsible for disposing the returned source.**\n *\n * @example\n * ```ts\n * const { snapshot, source } = await prefetchSourceAndKeep({ fetch: fetchUsers, limit: 20 });\n * // hydrate HTML with snapshot, pass source to client\n * ```\n */\nexport async function prefetchSourceAndKeep<T, TFilter = unknown, TSort = unknown>(\n cfg: PrefetchConfig<T, TFilter, TSort>,\n): Promise<{ snapshot: SourceSnapshot<T>; source: RemoteSource<T, TFilter, TSort> }> {\n const source = createRemoteSource<T, TFilter, TSort>({ ...cfg, autoFetch: false });\n\n await source.refresh();\n\n if (source.meta.error) {\n source.dispose();\n throw source.meta.error;\n }\n\n return { snapshot: buildSnapshot(source), source };\n}\n"],"mappings":"sCAMA,IAAM,EAAoC,GAA+D,CACvG,IAAM,EAAI,EAAO,MAEjB,MAAO,CACL,MAAO,EAAO,QACd,GAAI,EAAE,QAAU,CAAE,OAAQ,EAAE,MAAO,EACnC,KAAM,EAAO,KAAK,WAClB,MAAO,EAAO,KAAK,UACrB,CACF,EAYA,eAAsB,EACpB,EAC4B,CAC5B,IAAM,EAAS,EAAA,mBAAsC,CAAE,GAAG,EAAK,UAAW,EAAM,CAAC,EAEjF,GAAI,CAGF,GAFA,MAAM,EAAO,QAAQ,EAEjB,EAAO,KAAK,MAAO,MAAM,EAAO,KAAK,MAEzC,OAAO,EAAc,CAAM,CAC7B,QAAU,CACR,EAAO,QAAQ,CACjB,CACF,CAeA,eAAsB,EACpB,EACmF,CACnF,IAAM,EAAS,EAAA,mBAAsC,CAAE,GAAG,EAAK,UAAW,EAAM,CAAC,EAIjF,GAFA,MAAM,EAAO,QAAQ,EAEjB,EAAO,KAAK,MAEd,MADA,EAAO,QAAQ,EACT,EAAO,KAAK,MAGpB,MAAO,CAAE,SAAU,EAAc,CAAM,EAAG,QAAO,CACnD"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RemoteConfig, RemoteSource, SourceSnapshot } from './types';
|
|
2
|
+
type PrefetchConfig<T, TFilter, TSort> = Omit<RemoteConfig<T, TFilter, TSort>, 'autoFetch' | 'refreshInterval'>;
|
|
3
|
+
/**
|
|
4
|
+
* Fetches a single page of data and returns a serialisable `SourceSnapshot`.
|
|
5
|
+
* The underlying source is created, fetched, then immediately disposed.
|
|
6
|
+
* Use for SSR / static-generation where you only need the data, not a live source.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const snapshot = await prefetchSource({ fetch: fetchUsers, limit: 20 });
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function prefetchSource<T, TFilter = unknown, TSort = unknown>(cfg: PrefetchConfig<T, TFilter, TSort>): Promise<SourceSnapshot<T>>;
|
|
14
|
+
/**
|
|
15
|
+
* Fetches a single page of data and returns both a serialisable `SourceSnapshot` and the
|
|
16
|
+
* still-live `RemoteSource`. Useful when you need the snapshot for SSR serialisation
|
|
17
|
+
* **and** the live source for subsequent client-side updates without a double-fetch.
|
|
18
|
+
*
|
|
19
|
+
* **The caller is responsible for disposing the returned source.**
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const { snapshot, source } = await prefetchSourceAndKeep({ fetch: fetchUsers, limit: 20 });
|
|
24
|
+
* // hydrate HTML with snapshot, pass source to client
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function prefetchSourceAndKeep<T, TFilter = unknown, TSort = unknown>(cfg: PrefetchConfig<T, TFilter, TSort>): Promise<{
|
|
28
|
+
snapshot: SourceSnapshot<T>;
|
|
29
|
+
source: RemoteSource<T, TFilter, TSort>;
|
|
30
|
+
}>;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=prefetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefetch.d.ts","sourceRoot":"","sources":["../src/prefetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAI1E,KAAK,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,iBAAiB,CAAC,CAAC;AAahH;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EACxE,GAAG,EAAE,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,GACrC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAY5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EAC/E,GAAG,EAAE,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,GACrC,OAAO,CAAC;IAAE,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IAAC,MAAM,EAAE,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAA;CAAE,CAAC,CAWnF"}
|