@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.
Files changed (118) hide show
  1. package/README.md +86 -0
  2. package/dist/_dev.d.ts +2 -0
  3. package/dist/_dev.d.ts.map +1 -0
  4. package/dist/_utils.cjs +2 -0
  5. package/dist/_utils.cjs.map +1 -0
  6. package/dist/_utils.d.ts +8 -0
  7. package/dist/_utils.d.ts.map +1 -0
  8. package/dist/_utils.js +7 -0
  9. package/dist/_utils.js.map +1 -0
  10. package/dist/applyQuery.cjs +2 -0
  11. package/dist/applyQuery.cjs.map +1 -0
  12. package/dist/applyQuery.d.ts +20 -0
  13. package/dist/applyQuery.d.ts.map +1 -0
  14. package/dist/applyQuery.js +8 -0
  15. package/dist/applyQuery.js.map +1 -0
  16. package/dist/asyncSource.cjs +2 -0
  17. package/dist/asyncSource.cjs.map +1 -0
  18. package/dist/asyncSource.d.ts +62 -0
  19. package/dist/asyncSource.d.ts.map +1 -0
  20. package/dist/asyncSource.js +40 -0
  21. package/dist/asyncSource.js.map +1 -0
  22. package/dist/codecs.cjs +2 -0
  23. package/dist/codecs.cjs.map +1 -0
  24. package/dist/codecs.d.ts +30 -0
  25. package/dist/codecs.d.ts.map +1 -0
  26. package/dist/codecs.js +36 -0
  27. package/dist/codecs.js.map +1 -0
  28. package/dist/core.cjs +2 -0
  29. package/dist/core.cjs.map +1 -0
  30. package/dist/core.d.ts +47 -0
  31. package/dist/core.d.ts.map +1 -0
  32. package/dist/core.js +59 -0
  33. package/dist/core.js.map +1 -0
  34. package/dist/cursorSource.cjs +2 -0
  35. package/dist/cursorSource.cjs.map +1 -0
  36. package/dist/cursorSource.d.ts +4 -0
  37. package/dist/cursorSource.d.ts.map +1 -0
  38. package/dist/cursorSource.js +135 -0
  39. package/dist/cursorSource.js.map +1 -0
  40. package/dist/derive.cjs +2 -0
  41. package/dist/derive.cjs.map +1 -0
  42. package/dist/derive.d.ts +28 -0
  43. package/dist/derive.d.ts.map +1 -0
  44. package/dist/derive.js +40 -0
  45. package/dist/derive.js.map +1 -0
  46. package/dist/errors.cjs +2 -0
  47. package/dist/errors.cjs.map +1 -0
  48. package/dist/errors.d.ts +62 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.js +29 -0
  51. package/dist/errors.js.map +1 -0
  52. package/dist/fetchManager.cjs +2 -0
  53. package/dist/fetchManager.cjs.map +1 -0
  54. package/dist/fetchManager.d.ts +27 -0
  55. package/dist/fetchManager.d.ts.map +1 -0
  56. package/dist/fetchManager.js +41 -0
  57. package/dist/fetchManager.js.map +1 -0
  58. package/dist/index.cjs +1 -0
  59. package/dist/index.d.ts +16 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +14 -0
  62. package/dist/infiniteSource.cjs +2 -0
  63. package/dist/infiniteSource.cjs.map +1 -0
  64. package/dist/infiniteSource.d.ts +4 -0
  65. package/dist/infiniteSource.d.ts.map +1 -0
  66. package/dist/infiniteSource.js +135 -0
  67. package/dist/infiniteSource.js.map +1 -0
  68. package/dist/localSource.cjs +2 -0
  69. package/dist/localSource.cjs.map +1 -0
  70. package/dist/localSource.d.ts +24 -0
  71. package/dist/localSource.d.ts.map +1 -0
  72. package/dist/localSource.js +130 -0
  73. package/dist/localSource.js.map +1 -0
  74. package/dist/merge.cjs +2 -0
  75. package/dist/merge.cjs.map +1 -0
  76. package/dist/merge.d.ts +30 -0
  77. package/dist/merge.d.ts.map +1 -0
  78. package/dist/merge.js +34 -0
  79. package/dist/merge.js.map +1 -0
  80. package/dist/pagination.cjs +2 -0
  81. package/dist/pagination.cjs.map +1 -0
  82. package/dist/pagination.d.ts +31 -0
  83. package/dist/pagination.d.ts.map +1 -0
  84. package/dist/pagination.js +34 -0
  85. package/dist/pagination.js.map +1 -0
  86. package/dist/prefetch.cjs +2 -0
  87. package/dist/prefetch.cjs.map +1 -0
  88. package/dist/prefetch.d.ts +32 -0
  89. package/dist/prefetch.d.ts.map +1 -0
  90. package/dist/prefetch.js +38 -0
  91. package/dist/prefetch.js.map +1 -0
  92. package/dist/presets.cjs +2 -0
  93. package/dist/presets.cjs.map +1 -0
  94. package/dist/presets.d.ts +12 -0
  95. package/dist/presets.d.ts.map +1 -0
  96. package/dist/presets.js +24 -0
  97. package/dist/presets.js.map +1 -0
  98. package/dist/remoteSource.cjs +2 -0
  99. package/dist/remoteSource.cjs.map +1 -0
  100. package/dist/remoteSource.d.ts +4 -0
  101. package/dist/remoteSource.d.ts.map +1 -0
  102. package/dist/remoteSource.js +175 -0
  103. package/dist/remoteSource.js.map +1 -0
  104. package/dist/sourcerer.cjs +2 -0
  105. package/dist/sourcerer.cjs.map +1 -0
  106. package/dist/sourcerer.iife.js +2 -0
  107. package/dist/sourcerer.iife.js.map +1 -0
  108. package/dist/sourcerer.js +2 -0
  109. package/dist/sourcerer.js.map +1 -0
  110. package/dist/state.cjs +2 -0
  111. package/dist/state.cjs.map +1 -0
  112. package/dist/state.d.ts +23 -0
  113. package/dist/state.d.ts.map +1 -0
  114. package/dist/state.js +14 -0
  115. package/dist/state.js.map +1 -0
  116. package/dist/types.d.ts +374 -0
  117. package/dist/types.d.ts.map +1 -0
  118. package/package.json +43 -0
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @vielzeug/sourcerer
2
+
3
+ > Typed reactive data sources for pagination, filtering, sorting, search, and infinite scroll.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@vielzeug/sourcerer)](https://www.npmjs.com/package/@vielzeug/sourcerer) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ `@vielzeug/sourcerer` is part of Vielzeug and ships as a TypeScript package with ESM+CJS output.
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ pnpm add @vielzeug/sourcerer
13
+ npm install @vielzeug/sourcerer
14
+ yarn add @vielzeug/sourcerer
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```ts
20
+ import { createLocalSource } from '@vielzeug/sourcerer';
21
+
22
+ const source = createLocalSource(
23
+ [
24
+ { id: 1, name: 'Ada' },
25
+ { id: 2, name: 'Grace' },
26
+ { id: 3, name: 'Linus' },
27
+ ],
28
+ { limit: 2 },
29
+ );
30
+
31
+ await source.search('a', { immediate: true });
32
+ console.log(source.current); // [{ id: 1, name: 'Ada' }]
33
+ console.log(source.meta.pageNumber); // 1
34
+ ```
35
+
36
+ ```ts
37
+ import { createRemoteSource } from '@vielzeug/sourcerer';
38
+
39
+ const source = createRemoteSource({
40
+ fetch: async ({ limit, page, search }, signal) => {
41
+ const res = await fetch(`/api/users?page=${page}&limit=${limit}&q=${search ?? ''}`, { signal });
42
+ return res.json(); // { items: User[], total: number }
43
+ },
44
+ limit: 20,
45
+ });
46
+
47
+ // autoFetch is true by default — initial data loads immediately
48
+ await source.ready();
49
+ console.log(source.current, source.meta.totalItems);
50
+ ```
51
+
52
+ ## Sources Overview
53
+
54
+ | Factory | Data model | Navigation |
55
+ | ------------------------ | ------------------ | ------------------------- |
56
+ | `createLocalSource()` | In-memory array | Page number |
57
+ | `createRemoteSource()` | Async server fetch | Page number |
58
+ | `createCursorSource()` | Async server fetch | Cursor tokens (next/prev) |
59
+ | `createInfiniteSource()` | Async server fetch | Append (load more) |
60
+
61
+ ## Error Handling
62
+
63
+ All async sources expose `meta.error` as a typed `SourceError | null` — not a plain string.
64
+ `SourceError` extends `Error` and carries structured context:
65
+
66
+ ```ts
67
+ import { sourceState } from '@vielzeug/sourcerer';
68
+
69
+ const state = sourceState(source);
70
+ if (state.status === 'error') {
71
+ console.error(state.error.message); // human-readable message
72
+ console.error(state.error.cause); // original thrown value
73
+ console.error(state.error.context); // structured context bag (query fields, kind, etc.)
74
+ }
75
+ ```
76
+
77
+ ## Documentation
78
+
79
+ - [Overview](https://vielzeug.dev/sourcerer/)
80
+ - [Usage Guide](https://vielzeug.dev/sourcerer/usage)
81
+ - [API Reference](https://vielzeug.dev/sourcerer/api)
82
+ - [Examples](https://vielzeug.dev/sourcerer/examples)
83
+
84
+ ## License
85
+
86
+ MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) — part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
package/dist/_dev.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=_dev.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_dev.d.ts","sourceRoot":"","sources":["../src/_dev.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ let e=require("@vielzeug/arsenal");var t=e.hash,n=e=>e instanceof Error?e.message:typeof e==`string`&&e.length>0?e:`Request failed`,r=t=>(0,e.backoff)(t);exports.defaultKeyOf=t,exports.defaultRetryDelay=r,exports.extractError=n;
2
+ //# sourceMappingURL=_utils.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_utils.cjs","names":[],"sources":["../src/_utils.ts"],"sourcesContent":["import { backoff, hash, retry } from '@vielzeug/arsenal';\n\nexport { retry };\n\n// Internal stable-key helper — used by source factories, not exposed as public API.\nexport const defaultKeyOf = hash;\n\n/** Extracts a user-facing error message from a caught exception. */\nexport const extractError = (reason: unknown): string => {\n if (reason instanceof Error) return reason.message;\n\n if (typeof reason === 'string' && reason.length > 0) return reason;\n\n return 'Request failed';\n};\n\n/** Default exponential backoff: 1 s, 2 s, 4 s, … capped at 30 s. Receives a 0-indexed failure count. */\nexport const defaultRetryDelay = (attempt: number): number => backoff(attempt);\n"],"mappings":"mCAKA,IAAa,EAAe,EAAA,KAGf,EAAgB,GACvB,aAAkB,MAAc,EAAO,QAEvC,OAAO,GAAW,UAAY,EAAO,OAAS,EAAU,EAErD,iBAII,EAAqB,IAAA,EAAA,EAAA,QAAA,CAAoC,CAAO"}
@@ -0,0 +1,8 @@
1
+ import { hash, retry } from '@vielzeug/arsenal';
2
+ export { retry };
3
+ export declare const defaultKeyOf: typeof hash;
4
+ /** Extracts a user-facing error message from a caught exception. */
5
+ export declare const extractError: (reason: unknown) => string;
6
+ /** Default exponential backoff: 1 s, 2 s, 4 s, … capped at 30 s. Receives a 0-indexed failure count. */
7
+ export declare const defaultRetryDelay: (attempt: number) => number;
8
+ //# sourceMappingURL=_utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_utils.d.ts","sourceRoot":"","sources":["../src/_utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,IAAI,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAAE,KAAK,EAAE,CAAC;AAGjB,eAAO,MAAM,YAAY,aAAO,CAAC;AAEjC,oEAAoE;AACpE,eAAO,MAAM,YAAY,GAAI,QAAQ,OAAO,KAAG,MAM9C,CAAC;AAEF,wGAAwG;AACxG,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,KAAG,MAA0B,CAAC"}
package/dist/_utils.js ADDED
@@ -0,0 +1,7 @@
1
+ import { backoff as e, hash as t, retry as n } from "@vielzeug/arsenal";
2
+ //#region src/_utils.ts
3
+ var r = t, i = (e) => e instanceof Error ? e.message : typeof e == "string" && e.length > 0 ? e : "Request failed", a = (t) => e(t);
4
+ //#endregion
5
+ export { r as defaultKeyOf, a as defaultRetryDelay, i as extractError, n as retry };
6
+
7
+ //# sourceMappingURL=_utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_utils.js","names":[],"sources":["../src/_utils.ts"],"sourcesContent":["import { backoff, hash, retry } from '@vielzeug/arsenal';\n\nexport { retry };\n\n// Internal stable-key helper — used by source factories, not exposed as public API.\nexport const defaultKeyOf = hash;\n\n/** Extracts a user-facing error message from a caught exception. */\nexport const extractError = (reason: unknown): string => {\n if (reason instanceof Error) return reason.message;\n\n if (typeof reason === 'string' && reason.length > 0) return reason;\n\n return 'Request failed';\n};\n\n/** Default exponential backoff: 1 s, 2 s, 4 s, … capped at 30 s. Receives a 0-indexed failure count. */\nexport const defaultRetryDelay = (attempt: number): number => backoff(attempt);\n"],"mappings":";;AAKA,IAAa,IAAe,GAGf,KAAgB,MACvB,aAAkB,QAAc,EAAO,UAEvC,OAAO,KAAW,YAAY,EAAO,SAAS,IAAU,IAErD,kBAII,KAAqB,MAA4B,EAAQ,CAAO"}
@@ -0,0 +1,2 @@
1
+ function e(e,t){return e.patch(t)}exports.applyQuery=e;
2
+ //# sourceMappingURL=applyQuery.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applyQuery.cjs","names":[],"sources":["../src/applyQuery.ts"],"sourcesContent":["import type { SourceQuery } from './types';\n\n/**\n * Generic query applicator — delegates to `source.patch(changes)`.\n * Use this with `decodeQuery()` output to restore URL state onto any source:\n *\n * @example\n * ```ts\n * const q = decodeQuery(new URL(location.href).searchParams, { defaultLimit: 20 });\n * await applyQuery(source, q);\n * ```\n *\n * ⚠️ `CursorSource` and `InfiniteSource` have no page-number concept (keyset/append navigation) —\n * their `patch()` only reads `limit`/`search` and silently ignores a `page` field. This compiles\n * (both accept `Partial<SourceQuery>`-shaped input) but a `page` value from `decodeQuery()` is a\n * no-op on those two source types.\n */\nexport function applyQuery<T extends { patch(changes: Partial<SourceQuery>): Promise<void> }>(\n source: T,\n changes: Partial<SourceQuery>,\n): Promise<void> {\n return source.patch(changes);\n}\n"],"mappings":"AAiBA,SAAgB,EACd,EACA,EACe,CACf,OAAO,EAAO,MAAM,CAAO,CAC7B"}
@@ -0,0 +1,20 @@
1
+ import type { SourceQuery } from './types';
2
+ /**
3
+ * Generic query applicator — delegates to `source.patch(changes)`.
4
+ * Use this with `decodeQuery()` output to restore URL state onto any source:
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const q = decodeQuery(new URL(location.href).searchParams, { defaultLimit: 20 });
9
+ * await applyQuery(source, q);
10
+ * ```
11
+ *
12
+ * ⚠️ `CursorSource` and `InfiniteSource` have no page-number concept (keyset/append navigation) —
13
+ * their `patch()` only reads `limit`/`search` and silently ignores a `page` field. This compiles
14
+ * (both accept `Partial<SourceQuery>`-shaped input) but a `page` value from `decodeQuery()` is a
15
+ * no-op on those two source types.
16
+ */
17
+ export declare function applyQuery<T extends {
18
+ patch(changes: Partial<SourceQuery>): Promise<void>;
19
+ }>(source: T, changes: Partial<SourceQuery>): Promise<void>;
20
+ //# sourceMappingURL=applyQuery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applyQuery.d.ts","sourceRoot":"","sources":["../src/applyQuery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS;IAAE,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,EAC1F,MAAM,EAAE,CAAC,EACT,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,IAAI,CAAC,CAEf"}
@@ -0,0 +1,8 @@
1
+ //#region src/applyQuery.ts
2
+ function e(e, t) {
3
+ return e.patch(t);
4
+ }
5
+ //#endregion
6
+ export { e as applyQuery };
7
+
8
+ //# sourceMappingURL=applyQuery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applyQuery.js","names":[],"sources":["../src/applyQuery.ts"],"sourcesContent":["import type { SourceQuery } from './types';\n\n/**\n * Generic query applicator — delegates to `source.patch(changes)`.\n * Use this with `decodeQuery()` output to restore URL state onto any source:\n *\n * @example\n * ```ts\n * const q = decodeQuery(new URL(location.href).searchParams, { defaultLimit: 20 });\n * await applyQuery(source, q);\n * ```\n *\n * ⚠️ `CursorSource` and `InfiniteSource` have no page-number concept (keyset/append navigation) —\n * their `patch()` only reads `limit`/`search` and silently ignores a `page` field. This compiles\n * (both accept `Partial<SourceQuery>`-shaped input) but a `page` value from `decodeQuery()` is a\n * no-op on those two source types.\n */\nexport function applyQuery<T extends { patch(changes: Partial<SourceQuery>): Promise<void> }>(\n source: T,\n changes: Partial<SourceQuery>,\n): Promise<void> {\n return source.patch(changes);\n}\n"],"mappings":";AAiBA,SAAgB,EACd,GACA,GACe;CACf,OAAO,EAAO,MAAM,CAAO;AAC7B"}
@@ -0,0 +1,2 @@
1
+ const e=require("./_utils.cjs"),t=require("./core.cjs"),n=require("./fetchManager.cjs");function r(r,i=e.defaultKeyOf,a){let o=t.createSourceCore({onBeforeNotify:a}),s=n.createFetchManager(i),c=r.debounceMs??300,l=r.retry?.attempts??0,u=r.retry?.delay??e.defaultRetryDelay,d=r.autoFetch!==!1,f,p=()=>{o.isDisposed||(f!==void 0&&(clearInterval(f),f=void 0),s.dispose(),o.dispose())};return{autoFetch:d,core:o,debounceMs:c,get disposalSignal(){return o.disposalSignal},dispose:p,get disposed(){return o.isDisposed},fetch(e,t,n){return s.run(e,t,n)},pendingCount:()=>s.pendingCount,ready(e){return o.ready(()=>s.pendingCount===0&&!o.isScheduled,e)},retryAttempts:l,retryDelay:u,startRefreshInterval(e){r.refreshInterval!==void 0&&r.refreshInterval>0&&(f=setInterval(()=>{o.isDisposed||e()},r.refreshInterval))},[Symbol.dispose]:p}}exports.createAsyncSource=r;
2
+ //# sourceMappingURL=asyncSource.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asyncSource.cjs","names":[],"sources":["../src/asyncSource.ts"],"sourcesContent":["import type { SourceCore } from './core';\n\nimport { defaultKeyOf, defaultRetryDelay } from './_utils';\nimport { createSourceCore } from './core';\nimport { createFetchManager } from './fetchManager';\n\n/**\n * Configuration subset common to all async source types.\n * Extracted to eliminate duplication across remote, cursor, and infinite sources.\n */\nexport type AsyncSourceConfig<TQuery> = Readonly<{\n autoFetch?: boolean;\n debounceMs?: number;\n queryKey?: (q: TQuery) => string;\n refreshInterval?: number;\n retry?: { attempts?: number; delay?: (attempt: number) => number };\n}>;\n\nexport type FetchExecutor<TQuery> = (q: TQuery, signal: AbortSignal, isLatest: () => boolean) => Promise<void>;\n\nexport type AsyncSourceInfra<TQuery> = {\n [Symbol.dispose](): void;\n /** `true` when `autoFetch !== false`. */\n readonly autoFetch: boolean;\n /** The underlying SourceCore — use directly for subscribe, schedule, cancelTimer, flush, isScheduled. */\n readonly core: SourceCore;\n /** Debounce delay in ms. */\n readonly debounceMs: number;\n /** `AbortSignal` aborted when `dispose()` is called. */\n readonly disposalSignal: AbortSignal;\n /** Dispose lifecycle: cancel refresh interval, dispose FM, dispose core. Idempotent. */\n dispose(): void;\n /** `true` after `dispose()`. */\n readonly disposed: boolean;\n /**\n * Run a deduplicated fetch for query `q`.\n * Calls `onCommit()` whenever pending count changes (for subscriber notification).\n */\n fetch(q: TQuery, execute: FetchExecutor<TQuery>, onCommit: () => void): Promise<void>;\n /** Number of currently in-flight requests. */\n pendingCount(): number;\n /** Resolves when idle (no in-flight requests, no pending debounce). */\n ready(timeout?: number): Promise<void>;\n /** Number of retry attempts after the first failure. */\n readonly retryAttempts: number;\n /** Returns the delay in ms before the nth retry. */\n readonly retryDelay: (attempt: number) => number;\n /**\n * Wire the refresh interval. Must be called once after construction, passing\n * the source-level `doUpdate` callback. Does nothing when `refreshInterval`\n * is unset or ≤ 0.\n */\n startRefreshInterval(doUpdate: () => void): void;\n};\n\n/**\n * Creates the shared infrastructure used by `createRemoteSource`, `createCursorSource`,\n * and `createInfiniteSource`. Eliminates duplication of lifecycle, config-defaults,\n * fetch-manager setup, dispose, ready, flush, and refresh-interval logic.\n *\n * Each source factory composes this and adds its own query state, meta shape, and\n * source-specific methods. Pass `onBeforeNotify` to run `refreshMeta()` before\n * subscribers observe each notification — this eliminates the need for a parallel\n * listeners Set in each source factory.\n */\nexport function createAsyncSource<TQuery>(\n cfg: AsyncSourceConfig<TQuery>,\n keyOf: (q: TQuery) => string = defaultKeyOf as (q: TQuery) => string,\n onBeforeNotify?: () => void,\n): AsyncSourceInfra<TQuery> {\n const core = createSourceCore({ onBeforeNotify });\n const fm = createFetchManager<TQuery>(keyOf);\n const debounceMs = cfg.debounceMs ?? 300;\n const retryAttempts = cfg.retry?.attempts ?? 0;\n const retryDelay = cfg.retry?.delay ?? defaultRetryDelay;\n const autoFetch = cfg.autoFetch !== false;\n\n let refreshTimer: ReturnType<typeof setInterval> | undefined;\n\n const dispose = (): void => {\n if (core.isDisposed) return;\n\n if (refreshTimer !== undefined) {\n clearInterval(refreshTimer);\n refreshTimer = undefined;\n }\n\n fm.dispose();\n core.dispose();\n };\n\n return {\n autoFetch,\n core,\n debounceMs,\n get disposalSignal() {\n return core.disposalSignal;\n },\n\n dispose,\n\n get disposed() {\n return core.isDisposed;\n },\n\n fetch(q, execute, onCommit) {\n return fm.run(q, execute, onCommit);\n },\n\n pendingCount: () => fm.pendingCount,\n\n ready(timeout) {\n return core.ready(() => fm.pendingCount === 0 && !core.isScheduled, timeout);\n },\n retryAttempts,\n\n retryDelay,\n\n startRefreshInterval(doUpdate) {\n if (cfg.refreshInterval !== undefined && cfg.refreshInterval > 0) {\n refreshTimer = setInterval(() => {\n if (!core.isDisposed) doUpdate();\n }, cfg.refreshInterval);\n }\n },\n\n [Symbol.dispose]: dispose,\n };\n}\n"],"mappings":"wFAiEA,SAAgB,EACd,EACA,EAA+B,EAAA,aAC/B,EAC0B,CAC1B,IAAM,EAAO,EAAA,iBAAiB,CAAE,gBAAe,CAAC,EAC1C,EAAK,EAAA,mBAA2B,CAAK,EACrC,EAAa,EAAI,YAAc,IAC/B,EAAgB,EAAI,OAAO,UAAY,EACvC,EAAa,EAAI,OAAO,OAAS,EAAA,kBACjC,EAAY,EAAI,YAAc,GAEhC,EAEE,MAAsB,CACtB,EAAK,aAEL,IAAiB,IAAA,KACnB,cAAc,CAAY,EAC1B,EAAe,IAAA,IAGjB,EAAG,QAAQ,EACX,EAAK,QAAQ,EACf,EAEA,MAAO,CACL,YACA,OACA,aACA,IAAI,gBAAiB,CACnB,OAAO,EAAK,cACd,EAEA,UAEA,IAAI,UAAW,CACb,OAAO,EAAK,UACd,EAEA,MAAM,EAAG,EAAS,EAAU,CAC1B,OAAO,EAAG,IAAI,EAAG,EAAS,CAAQ,CACpC,EAEA,iBAAoB,EAAG,aAEvB,MAAM,EAAS,CACb,OAAO,EAAK,UAAY,EAAG,eAAiB,GAAK,CAAC,EAAK,YAAa,CAAO,CAC7E,EACA,gBAEA,aAEA,qBAAqB,EAAU,CACzB,EAAI,kBAAoB,IAAA,IAAa,EAAI,gBAAkB,IAC7D,EAAe,gBAAkB,CAC1B,EAAK,YAAY,EAAS,CACjC,EAAG,EAAI,eAAe,EAE1B,GAEC,OAAO,SAAU,CACpB,CACF"}
@@ -0,0 +1,62 @@
1
+ import type { SourceCore } from './core';
2
+ /**
3
+ * Configuration subset common to all async source types.
4
+ * Extracted to eliminate duplication across remote, cursor, and infinite sources.
5
+ */
6
+ export type AsyncSourceConfig<TQuery> = Readonly<{
7
+ autoFetch?: boolean;
8
+ debounceMs?: number;
9
+ queryKey?: (q: TQuery) => string;
10
+ refreshInterval?: number;
11
+ retry?: {
12
+ attempts?: number;
13
+ delay?: (attempt: number) => number;
14
+ };
15
+ }>;
16
+ export type FetchExecutor<TQuery> = (q: TQuery, signal: AbortSignal, isLatest: () => boolean) => Promise<void>;
17
+ export type AsyncSourceInfra<TQuery> = {
18
+ [Symbol.dispose](): void;
19
+ /** `true` when `autoFetch !== false`. */
20
+ readonly autoFetch: boolean;
21
+ /** The underlying SourceCore — use directly for subscribe, schedule, cancelTimer, flush, isScheduled. */
22
+ readonly core: SourceCore;
23
+ /** Debounce delay in ms. */
24
+ readonly debounceMs: number;
25
+ /** `AbortSignal` aborted when `dispose()` is called. */
26
+ readonly disposalSignal: AbortSignal;
27
+ /** Dispose lifecycle: cancel refresh interval, dispose FM, dispose core. Idempotent. */
28
+ dispose(): void;
29
+ /** `true` after `dispose()`. */
30
+ readonly disposed: boolean;
31
+ /**
32
+ * Run a deduplicated fetch for query `q`.
33
+ * Calls `onCommit()` whenever pending count changes (for subscriber notification).
34
+ */
35
+ fetch(q: TQuery, execute: FetchExecutor<TQuery>, onCommit: () => void): Promise<void>;
36
+ /** Number of currently in-flight requests. */
37
+ pendingCount(): number;
38
+ /** Resolves when idle (no in-flight requests, no pending debounce). */
39
+ ready(timeout?: number): Promise<void>;
40
+ /** Number of retry attempts after the first failure. */
41
+ readonly retryAttempts: number;
42
+ /** Returns the delay in ms before the nth retry. */
43
+ readonly retryDelay: (attempt: number) => number;
44
+ /**
45
+ * Wire the refresh interval. Must be called once after construction, passing
46
+ * the source-level `doUpdate` callback. Does nothing when `refreshInterval`
47
+ * is unset or ≤ 0.
48
+ */
49
+ startRefreshInterval(doUpdate: () => void): void;
50
+ };
51
+ /**
52
+ * Creates the shared infrastructure used by `createRemoteSource`, `createCursorSource`,
53
+ * and `createInfiniteSource`. Eliminates duplication of lifecycle, config-defaults,
54
+ * fetch-manager setup, dispose, ready, flush, and refresh-interval logic.
55
+ *
56
+ * Each source factory composes this and adds its own query state, meta shape, and
57
+ * source-specific methods. Pass `onBeforeNotify` to run `refreshMeta()` before
58
+ * subscribers observe each notification — this eliminates the need for a parallel
59
+ * listeners Set in each source factory.
60
+ */
61
+ export declare function createAsyncSource<TQuery>(cfg: AsyncSourceConfig<TQuery>, keyOf?: (q: TQuery) => string, onBeforeNotify?: () => void): AsyncSourceInfra<TQuery>;
62
+ //# sourceMappingURL=asyncSource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asyncSource.d.ts","sourceRoot":"","sources":["../src/asyncSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAMzC;;;GAGG;AACH,MAAM,MAAM,iBAAiB,CAAC,MAAM,IAAI,QAAQ,CAAC;IAC/C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;KAAE,CAAC;CACpE,CAAC,CAAC;AAEH,MAAM,MAAM,aAAa,CAAC,MAAM,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE/G,MAAM,MAAM,gBAAgB,CAAC,MAAM,IAAI;IACrC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;IACzB,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,yGAAyG;IACzG,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,4BAA4B;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC;IACrC,wFAAwF;IACxF,OAAO,IAAI,IAAI,CAAC;IAChB,gCAAgC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B;;;OAGG;IACH,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtF,8CAA8C;IAC9C,YAAY,IAAI,MAAM,CAAC;IACvB,uEAAuE;IACvE,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,wDAAwD;IACxD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,oDAAoD;IACpD,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACjD;;;;OAIG;IACH,oBAAoB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CAClD,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EACtC,GAAG,EAAE,iBAAiB,CAAC,MAAM,CAAC,EAC9B,KAAK,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAA8C,EACpE,cAAc,CAAC,EAAE,MAAM,IAAI,GAC1B,gBAAgB,CAAC,MAAM,CAAC,CA2D1B"}
@@ -0,0 +1,40 @@
1
+ import { defaultKeyOf as e, defaultRetryDelay as t } from "./_utils.js";
2
+ import { createSourceCore as n } from "./core.js";
3
+ import { createFetchManager as r } from "./fetchManager.js";
4
+ //#region src/asyncSource.ts
5
+ function i(i, a = e, o) {
6
+ let s = n({ onBeforeNotify: o }), c = r(a), l = i.debounceMs ?? 300, u = i.retry?.attempts ?? 0, d = i.retry?.delay ?? t, f = i.autoFetch !== !1, p, m = () => {
7
+ s.isDisposed || (p !== void 0 && (clearInterval(p), p = void 0), c.dispose(), s.dispose());
8
+ };
9
+ return {
10
+ autoFetch: f,
11
+ core: s,
12
+ debounceMs: l,
13
+ get disposalSignal() {
14
+ return s.disposalSignal;
15
+ },
16
+ dispose: m,
17
+ get disposed() {
18
+ return s.isDisposed;
19
+ },
20
+ fetch(e, t, n) {
21
+ return c.run(e, t, n);
22
+ },
23
+ pendingCount: () => c.pendingCount,
24
+ ready(e) {
25
+ return s.ready(() => c.pendingCount === 0 && !s.isScheduled, e);
26
+ },
27
+ retryAttempts: u,
28
+ retryDelay: d,
29
+ startRefreshInterval(e) {
30
+ i.refreshInterval !== void 0 && i.refreshInterval > 0 && (p = setInterval(() => {
31
+ s.isDisposed || e();
32
+ }, i.refreshInterval));
33
+ },
34
+ [Symbol.dispose]: m
35
+ };
36
+ }
37
+ //#endregion
38
+ export { i as createAsyncSource };
39
+
40
+ //# sourceMappingURL=asyncSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asyncSource.js","names":[],"sources":["../src/asyncSource.ts"],"sourcesContent":["import type { SourceCore } from './core';\n\nimport { defaultKeyOf, defaultRetryDelay } from './_utils';\nimport { createSourceCore } from './core';\nimport { createFetchManager } from './fetchManager';\n\n/**\n * Configuration subset common to all async source types.\n * Extracted to eliminate duplication across remote, cursor, and infinite sources.\n */\nexport type AsyncSourceConfig<TQuery> = Readonly<{\n autoFetch?: boolean;\n debounceMs?: number;\n queryKey?: (q: TQuery) => string;\n refreshInterval?: number;\n retry?: { attempts?: number; delay?: (attempt: number) => number };\n}>;\n\nexport type FetchExecutor<TQuery> = (q: TQuery, signal: AbortSignal, isLatest: () => boolean) => Promise<void>;\n\nexport type AsyncSourceInfra<TQuery> = {\n [Symbol.dispose](): void;\n /** `true` when `autoFetch !== false`. */\n readonly autoFetch: boolean;\n /** The underlying SourceCore — use directly for subscribe, schedule, cancelTimer, flush, isScheduled. */\n readonly core: SourceCore;\n /** Debounce delay in ms. */\n readonly debounceMs: number;\n /** `AbortSignal` aborted when `dispose()` is called. */\n readonly disposalSignal: AbortSignal;\n /** Dispose lifecycle: cancel refresh interval, dispose FM, dispose core. Idempotent. */\n dispose(): void;\n /** `true` after `dispose()`. */\n readonly disposed: boolean;\n /**\n * Run a deduplicated fetch for query `q`.\n * Calls `onCommit()` whenever pending count changes (for subscriber notification).\n */\n fetch(q: TQuery, execute: FetchExecutor<TQuery>, onCommit: () => void): Promise<void>;\n /** Number of currently in-flight requests. */\n pendingCount(): number;\n /** Resolves when idle (no in-flight requests, no pending debounce). */\n ready(timeout?: number): Promise<void>;\n /** Number of retry attempts after the first failure. */\n readonly retryAttempts: number;\n /** Returns the delay in ms before the nth retry. */\n readonly retryDelay: (attempt: number) => number;\n /**\n * Wire the refresh interval. Must be called once after construction, passing\n * the source-level `doUpdate` callback. Does nothing when `refreshInterval`\n * is unset or ≤ 0.\n */\n startRefreshInterval(doUpdate: () => void): void;\n};\n\n/**\n * Creates the shared infrastructure used by `createRemoteSource`, `createCursorSource`,\n * and `createInfiniteSource`. Eliminates duplication of lifecycle, config-defaults,\n * fetch-manager setup, dispose, ready, flush, and refresh-interval logic.\n *\n * Each source factory composes this and adds its own query state, meta shape, and\n * source-specific methods. Pass `onBeforeNotify` to run `refreshMeta()` before\n * subscribers observe each notification — this eliminates the need for a parallel\n * listeners Set in each source factory.\n */\nexport function createAsyncSource<TQuery>(\n cfg: AsyncSourceConfig<TQuery>,\n keyOf: (q: TQuery) => string = defaultKeyOf as (q: TQuery) => string,\n onBeforeNotify?: () => void,\n): AsyncSourceInfra<TQuery> {\n const core = createSourceCore({ onBeforeNotify });\n const fm = createFetchManager<TQuery>(keyOf);\n const debounceMs = cfg.debounceMs ?? 300;\n const retryAttempts = cfg.retry?.attempts ?? 0;\n const retryDelay = cfg.retry?.delay ?? defaultRetryDelay;\n const autoFetch = cfg.autoFetch !== false;\n\n let refreshTimer: ReturnType<typeof setInterval> | undefined;\n\n const dispose = (): void => {\n if (core.isDisposed) return;\n\n if (refreshTimer !== undefined) {\n clearInterval(refreshTimer);\n refreshTimer = undefined;\n }\n\n fm.dispose();\n core.dispose();\n };\n\n return {\n autoFetch,\n core,\n debounceMs,\n get disposalSignal() {\n return core.disposalSignal;\n },\n\n dispose,\n\n get disposed() {\n return core.isDisposed;\n },\n\n fetch(q, execute, onCommit) {\n return fm.run(q, execute, onCommit);\n },\n\n pendingCount: () => fm.pendingCount,\n\n ready(timeout) {\n return core.ready(() => fm.pendingCount === 0 && !core.isScheduled, timeout);\n },\n retryAttempts,\n\n retryDelay,\n\n startRefreshInterval(doUpdate) {\n if (cfg.refreshInterval !== undefined && cfg.refreshInterval > 0) {\n refreshTimer = setInterval(() => {\n if (!core.isDisposed) doUpdate();\n }, cfg.refreshInterval);\n }\n },\n\n [Symbol.dispose]: dispose,\n };\n}\n"],"mappings":";;;;AAiEA,SAAgB,EACd,GACA,IAA+B,GAC/B,GAC0B;CAC1B,IAAM,IAAO,EAAiB,EAAE,kBAAe,CAAC,GAC1C,IAAK,EAA2B,CAAK,GACrC,IAAa,EAAI,cAAc,KAC/B,IAAgB,EAAI,OAAO,YAAY,GACvC,IAAa,EAAI,OAAO,SAAS,GACjC,IAAY,EAAI,cAAc,IAEhC,GAEE,UAAsB;EACtB,EAAK,eAEL,MAAiB,KAAA,MACnB,cAAc,CAAY,GAC1B,IAAe,KAAA,IAGjB,EAAG,QAAQ,GACX,EAAK,QAAQ;CACf;CAEA,OAAO;EACL;EACA;EACA;EACA,IAAI,iBAAiB;GACnB,OAAO,EAAK;EACd;EAEA;EAEA,IAAI,WAAW;GACb,OAAO,EAAK;EACd;EAEA,MAAM,GAAG,GAAS,GAAU;GAC1B,OAAO,EAAG,IAAI,GAAG,GAAS,CAAQ;EACpC;EAEA,oBAAoB,EAAG;EAEvB,MAAM,GAAS;GACb,OAAO,EAAK,YAAY,EAAG,iBAAiB,KAAK,CAAC,EAAK,aAAa,CAAO;EAC7E;EACA;EAEA;EAEA,qBAAqB,GAAU;GAC7B,AAAI,EAAI,oBAAoB,KAAA,KAAa,EAAI,kBAAkB,MAC7D,IAAe,kBAAkB;IAC/B,AAAK,EAAK,cAAY,EAAS;GACjC,GAAG,EAAI,eAAe;EAE1B;GAEC,OAAO,UAAU;CACpB;AACF"}
@@ -0,0 +1,2 @@
1
+ const e=require("./errors.cjs");var t=e=>{let t={...e.search&&{search:e.search},limit:String(e.limit),page:String(e.page)},n=e;return n.filter!==void 0&&(t.filter=JSON.stringify(n.filter)),n.sort!==void 0&&(t.sort=JSON.stringify(n.sort)),t},n=(t,n={})=>{let r=t instanceof URLSearchParams?Object.fromEntries(t.entries()):t,{defaultLimit:i=20,strict:a=!1}=n,o=(e,t)=>{if(e===void 0)return t;let n=Array.isArray(e)?e[0]:e,r=Number(n);return Number.isInteger(r)&&r>0?r:t},s=(t,n)=>{if(n===void 0)return;let r=Array.isArray(n)?n[0]:n;try{return JSON.parse(r)}catch{if(a)throw new e.SourcererError(`Invalid query param "${t}": ${r}`);return}},c=r.limit,l=r.page,u=r.search,d=r.filter,f=r.sort,p=d===void 0?void 0:s(`filter`,d),m=f===void 0?void 0:s(`sort`,f);return{...p!==void 0&&{filter:p},...m!==void 0&&{sort:m},...u!==void 0&&{search:Array.isArray(u)?u[0]:u},limit:o(c,i),page:o(l,1)}};exports.decodeQuery=n,exports.encodeQuery=t;
2
+ //# sourceMappingURL=codecs.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codecs.cjs","names":[],"sources":["../src/codecs.ts"],"sourcesContent":["import type { QueryParams, QueryParamsInput, RemoteSourceQuery, SourceQuery } from './types';\n\nimport { SourcererError } from './errors';\n\n/**\n * Serialises a `SourceQuery` or `RemoteSourceQuery` into plain URL-safe string params.\n *\n * ⚠️ `filter` and `sort` are JSON-stringified without a try/catch. A circular object reference\n * throws a native `TypeError` (\"Converting circular structure to JSON\") straight out of this\n * function — ensure filter/sort values are plain serialisable objects, or wrap the call yourself.\n *\n * ⚠️ Round-trip fidelity: `page` and `limit` must be positive integers. `encodeQuery` will\n * serialise any number (including 0), but `decodeQuery` clamps non-positive values to 1/defaultLimit.\n *\n * `encodeQuery` + `decodeQuery` form a round-trip pair: filter/sort values are JSON-stringified\n * on encode and JSON-parsed on decode.\n */\nexport const encodeQuery = <TFilter = unknown, TSort = unknown>(\n query: RemoteSourceQuery<TFilter, TSort> | SourceQuery,\n): QueryParams => {\n const base: QueryParams = {\n ...(query.search && { search: query.search }),\n limit: String(query.limit),\n page: String(query.page),\n };\n\n const rq = query as RemoteSourceQuery<TFilter, TSort>;\n\n if (rq.filter !== undefined) {\n base['filter'] = JSON.stringify(rq.filter);\n }\n\n if (rq.sort !== undefined) {\n base['sort'] = JSON.stringify(rq.sort);\n }\n\n return base;\n};\n\nexport type DecodeQueryOptions = Readonly<{\n defaultLimit?: number;\n strict?: boolean;\n}>;\n\n/**\n * Parses URL query params into a `Partial<RemoteSourceQuery<unknown, unknown>>`.\n * Accepts either a plain `Record<string, string | string[] | undefined>` or a `URLSearchParams` instance.\n *\n * - `filter` and `sort` are JSON-parsed and typed as `unknown` — validate and narrow them\n * with a runtime schema (e.g. Zod) before passing to `applyQuery`.\n * - `search` is omitted from the result when the param is absent (rather than defaulting to `''`).\n * - `limit` and `page` are parsed as positive integers; invalid values fall back to defaults.\n */\nexport const decodeQuery = (\n params: QueryParamsInput | URLSearchParams,\n options: DecodeQueryOptions = {},\n): Partial<RemoteSourceQuery<unknown, unknown>> => {\n const raw: QueryParamsInput =\n params instanceof URLSearchParams ? (Object.fromEntries(params.entries()) as QueryParamsInput) : params;\n\n const { defaultLimit = 20, strict = false } = options;\n\n const parsePositiveInt = (value: string | string[] | undefined, fallback: number): number => {\n if (value === undefined) return fallback;\n\n const str = Array.isArray(value) ? value[0] : value;\n const n = Number(str);\n\n return Number.isInteger(n) && n > 0 ? n : fallback;\n };\n\n const parseJson = (key: string, value: string | string[] | undefined): unknown => {\n if (value === undefined) return undefined;\n\n const str = Array.isArray(value) ? value[0] : value;\n\n try {\n return JSON.parse(str) as unknown;\n } catch {\n if (strict) throw new SourcererError(`Invalid query param \"${key}\": ${str}`);\n\n return undefined;\n }\n };\n\n const rawLimit = raw['limit'];\n const rawPage = raw['page'];\n const rawSearch = raw['search'];\n const rawFilter = raw['filter'];\n const rawSort = raw['sort'];\n\n const filter = rawFilter !== undefined ? parseJson('filter', rawFilter) : undefined;\n const sort = rawSort !== undefined ? parseJson('sort', rawSort) : undefined;\n\n const result: Partial<RemoteSourceQuery<unknown, unknown>> = {\n ...(filter !== undefined && { filter }),\n ...(sort !== undefined && { sort }),\n ...(rawSearch !== undefined && { search: Array.isArray(rawSearch) ? rawSearch[0] : rawSearch }),\n limit: parsePositiveInt(rawLimit, defaultLimit),\n page: parsePositiveInt(rawPage, 1),\n };\n\n return result;\n};\n"],"mappings":"gCAiBA,IAAa,EACX,GACgB,CAChB,IAAM,EAAoB,CACxB,GAAI,EAAM,QAAU,CAAE,OAAQ,EAAM,MAAO,EAC3C,MAAO,OAAO,EAAM,KAAK,EACzB,KAAM,OAAO,EAAM,IAAI,CACzB,EAEM,EAAK,EAUX,OARI,EAAG,SAAW,IAAA,KAChB,EAAK,OAAY,KAAK,UAAU,EAAG,MAAM,GAGvC,EAAG,OAAS,IAAA,KACd,EAAK,KAAU,KAAK,UAAU,EAAG,IAAI,GAGhC,CACT,EAgBa,GACX,EACA,EAA8B,CAAC,IACkB,CACjD,IAAM,EACJ,aAAkB,gBAAmB,OAAO,YAAY,EAAO,QAAQ,CAAC,EAAyB,EAE7F,CAAE,eAAe,GAAI,SAAS,IAAU,EAExC,GAAoB,EAAsC,IAA6B,CAC3F,GAAI,IAAU,IAAA,GAAW,OAAO,EAEhC,IAAM,EAAM,MAAM,QAAQ,CAAK,EAAI,EAAM,GAAK,EACxC,EAAI,OAAO,CAAG,EAEpB,OAAO,OAAO,UAAU,CAAC,GAAK,EAAI,EAAI,EAAI,CAC5C,EAEM,GAAa,EAAa,IAAkD,CAChF,GAAI,IAAU,IAAA,GAAW,OAEzB,IAAM,EAAM,MAAM,QAAQ,CAAK,EAAI,EAAM,GAAK,EAE9C,GAAI,CACF,OAAO,KAAK,MAAM,CAAG,CACvB,MAAQ,CACN,GAAI,EAAQ,MAAM,IAAI,EAAA,eAAe,wBAAwB,EAAI,KAAK,GAAK,EAE3E,MACF,CACF,EAEM,EAAW,EAAI,MACf,EAAU,EAAI,KACd,EAAY,EAAI,OAChB,EAAY,EAAI,OAChB,EAAU,EAAI,KAEd,EAAS,IAAc,IAAA,GAA6C,IAAA,GAAjC,EAAU,SAAU,CAAS,EAChE,EAAO,IAAY,IAAA,GAAyC,IAAA,GAA7B,EAAU,OAAQ,CAAO,EAU9D,MAAO,CAPL,GAAI,IAAW,IAAA,IAAa,CAAE,QAAO,EACrC,GAAI,IAAS,IAAA,IAAa,CAAE,MAAK,EACjC,GAAI,IAAc,IAAA,IAAa,CAAE,OAAQ,MAAM,QAAQ,CAAS,EAAI,EAAU,GAAK,CAAU,EAC7F,MAAO,EAAiB,EAAU,CAAY,EAC9C,KAAM,EAAiB,EAAS,CAAC,CAG5B,CACT"}
@@ -0,0 +1,30 @@
1
+ import type { QueryParams, QueryParamsInput, RemoteSourceQuery, SourceQuery } from './types';
2
+ /**
3
+ * Serialises a `SourceQuery` or `RemoteSourceQuery` into plain URL-safe string params.
4
+ *
5
+ * ⚠️ `filter` and `sort` are JSON-stringified without a try/catch. A circular object reference
6
+ * throws a native `TypeError` ("Converting circular structure to JSON") straight out of this
7
+ * function — ensure filter/sort values are plain serialisable objects, or wrap the call yourself.
8
+ *
9
+ * ⚠️ Round-trip fidelity: `page` and `limit` must be positive integers. `encodeQuery` will
10
+ * serialise any number (including 0), but `decodeQuery` clamps non-positive values to 1/defaultLimit.
11
+ *
12
+ * `encodeQuery` + `decodeQuery` form a round-trip pair: filter/sort values are JSON-stringified
13
+ * on encode and JSON-parsed on decode.
14
+ */
15
+ export declare const encodeQuery: <TFilter = unknown, TSort = unknown>(query: RemoteSourceQuery<TFilter, TSort> | SourceQuery) => QueryParams;
16
+ export type DecodeQueryOptions = Readonly<{
17
+ defaultLimit?: number;
18
+ strict?: boolean;
19
+ }>;
20
+ /**
21
+ * Parses URL query params into a `Partial<RemoteSourceQuery<unknown, unknown>>`.
22
+ * Accepts either a plain `Record<string, string | string[] | undefined>` or a `URLSearchParams` instance.
23
+ *
24
+ * - `filter` and `sort` are JSON-parsed and typed as `unknown` — validate and narrow them
25
+ * with a runtime schema (e.g. Zod) before passing to `applyQuery`.
26
+ * - `search` is omitted from the result when the param is absent (rather than defaulting to `''`).
27
+ * - `limit` and `page` are parsed as positive integers; invalid values fall back to defaults.
28
+ */
29
+ export declare const decodeQuery: (params: QueryParamsInput | URLSearchParams, options?: DecodeQueryOptions) => Partial<RemoteSourceQuery<unknown, unknown>>;
30
+ //# sourceMappingURL=codecs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codecs.d.ts","sourceRoot":"","sources":["../src/codecs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAI7F;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EAC5D,OAAO,iBAAiB,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,WAAW,KACrD,WAkBF,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,GACtB,QAAQ,gBAAgB,GAAG,eAAe,EAC1C,UAAS,kBAAuB,KAC/B,OAAO,CAAC,iBAAiB,CAAC,OAAO,EAAE,OAAO,CAAC,CA+C7C,CAAC"}
package/dist/codecs.js ADDED
@@ -0,0 +1,36 @@
1
+ import { SourcererError as e } from "./errors.js";
2
+ //#region src/codecs.ts
3
+ var t = (e) => {
4
+ let t = {
5
+ ...e.search && { search: e.search },
6
+ limit: String(e.limit),
7
+ page: String(e.page)
8
+ }, n = e;
9
+ return n.filter !== void 0 && (t.filter = JSON.stringify(n.filter)), n.sort !== void 0 && (t.sort = JSON.stringify(n.sort)), t;
10
+ }, n = (t, n = {}) => {
11
+ let r = t instanceof URLSearchParams ? Object.fromEntries(t.entries()) : t, { defaultLimit: i = 20, strict: a = !1 } = n, o = (e, t) => {
12
+ if (e === void 0) return t;
13
+ let n = Array.isArray(e) ? e[0] : e, r = Number(n);
14
+ return Number.isInteger(r) && r > 0 ? r : t;
15
+ }, s = (t, n) => {
16
+ if (n === void 0) return;
17
+ let r = Array.isArray(n) ? n[0] : n;
18
+ try {
19
+ return JSON.parse(r);
20
+ } catch {
21
+ if (a) throw new e(`Invalid query param "${t}": ${r}`);
22
+ return;
23
+ }
24
+ }, c = r.limit, l = r.page, u = r.search, d = r.filter, f = r.sort, p = d === void 0 ? void 0 : s("filter", d), m = f === void 0 ? void 0 : s("sort", f);
25
+ return {
26
+ ...p !== void 0 && { filter: p },
27
+ ...m !== void 0 && { sort: m },
28
+ ...u !== void 0 && { search: Array.isArray(u) ? u[0] : u },
29
+ limit: o(c, i),
30
+ page: o(l, 1)
31
+ };
32
+ };
33
+ //#endregion
34
+ export { n as decodeQuery, t as encodeQuery };
35
+
36
+ //# sourceMappingURL=codecs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codecs.js","names":[],"sources":["../src/codecs.ts"],"sourcesContent":["import type { QueryParams, QueryParamsInput, RemoteSourceQuery, SourceQuery } from './types';\n\nimport { SourcererError } from './errors';\n\n/**\n * Serialises a `SourceQuery` or `RemoteSourceQuery` into plain URL-safe string params.\n *\n * ⚠️ `filter` and `sort` are JSON-stringified without a try/catch. A circular object reference\n * throws a native `TypeError` (\"Converting circular structure to JSON\") straight out of this\n * function — ensure filter/sort values are plain serialisable objects, or wrap the call yourself.\n *\n * ⚠️ Round-trip fidelity: `page` and `limit` must be positive integers. `encodeQuery` will\n * serialise any number (including 0), but `decodeQuery` clamps non-positive values to 1/defaultLimit.\n *\n * `encodeQuery` + `decodeQuery` form a round-trip pair: filter/sort values are JSON-stringified\n * on encode and JSON-parsed on decode.\n */\nexport const encodeQuery = <TFilter = unknown, TSort = unknown>(\n query: RemoteSourceQuery<TFilter, TSort> | SourceQuery,\n): QueryParams => {\n const base: QueryParams = {\n ...(query.search && { search: query.search }),\n limit: String(query.limit),\n page: String(query.page),\n };\n\n const rq = query as RemoteSourceQuery<TFilter, TSort>;\n\n if (rq.filter !== undefined) {\n base['filter'] = JSON.stringify(rq.filter);\n }\n\n if (rq.sort !== undefined) {\n base['sort'] = JSON.stringify(rq.sort);\n }\n\n return base;\n};\n\nexport type DecodeQueryOptions = Readonly<{\n defaultLimit?: number;\n strict?: boolean;\n}>;\n\n/**\n * Parses URL query params into a `Partial<RemoteSourceQuery<unknown, unknown>>`.\n * Accepts either a plain `Record<string, string | string[] | undefined>` or a `URLSearchParams` instance.\n *\n * - `filter` and `sort` are JSON-parsed and typed as `unknown` — validate and narrow them\n * with a runtime schema (e.g. Zod) before passing to `applyQuery`.\n * - `search` is omitted from the result when the param is absent (rather than defaulting to `''`).\n * - `limit` and `page` are parsed as positive integers; invalid values fall back to defaults.\n */\nexport const decodeQuery = (\n params: QueryParamsInput | URLSearchParams,\n options: DecodeQueryOptions = {},\n): Partial<RemoteSourceQuery<unknown, unknown>> => {\n const raw: QueryParamsInput =\n params instanceof URLSearchParams ? (Object.fromEntries(params.entries()) as QueryParamsInput) : params;\n\n const { defaultLimit = 20, strict = false } = options;\n\n const parsePositiveInt = (value: string | string[] | undefined, fallback: number): number => {\n if (value === undefined) return fallback;\n\n const str = Array.isArray(value) ? value[0] : value;\n const n = Number(str);\n\n return Number.isInteger(n) && n > 0 ? n : fallback;\n };\n\n const parseJson = (key: string, value: string | string[] | undefined): unknown => {\n if (value === undefined) return undefined;\n\n const str = Array.isArray(value) ? value[0] : value;\n\n try {\n return JSON.parse(str) as unknown;\n } catch {\n if (strict) throw new SourcererError(`Invalid query param \"${key}\": ${str}`);\n\n return undefined;\n }\n };\n\n const rawLimit = raw['limit'];\n const rawPage = raw['page'];\n const rawSearch = raw['search'];\n const rawFilter = raw['filter'];\n const rawSort = raw['sort'];\n\n const filter = rawFilter !== undefined ? parseJson('filter', rawFilter) : undefined;\n const sort = rawSort !== undefined ? parseJson('sort', rawSort) : undefined;\n\n const result: Partial<RemoteSourceQuery<unknown, unknown>> = {\n ...(filter !== undefined && { filter }),\n ...(sort !== undefined && { sort }),\n ...(rawSearch !== undefined && { search: Array.isArray(rawSearch) ? rawSearch[0] : rawSearch }),\n limit: parsePositiveInt(rawLimit, defaultLimit),\n page: parsePositiveInt(rawPage, 1),\n };\n\n return result;\n};\n"],"mappings":";;AAiBA,IAAa,KACX,MACgB;CAChB,IAAM,IAAoB;EACxB,GAAI,EAAM,UAAU,EAAE,QAAQ,EAAM,OAAO;EAC3C,OAAO,OAAO,EAAM,KAAK;EACzB,MAAM,OAAO,EAAM,IAAI;CACzB,GAEM,IAAK;CAUX,OARI,EAAG,WAAW,KAAA,MAChB,EAAK,SAAY,KAAK,UAAU,EAAG,MAAM,IAGvC,EAAG,SAAS,KAAA,MACd,EAAK,OAAU,KAAK,UAAU,EAAG,IAAI,IAGhC;AACT,GAgBa,KACX,GACA,IAA8B,CAAC,MACkB;CACjD,IAAM,IACJ,aAAkB,kBAAmB,OAAO,YAAY,EAAO,QAAQ,CAAC,IAAyB,GAE7F,EAAE,kBAAe,IAAI,YAAS,OAAU,GAExC,KAAoB,GAAsC,MAA6B;EAC3F,IAAI,MAAU,KAAA,GAAW,OAAO;EAEhC,IAAM,IAAM,MAAM,QAAQ,CAAK,IAAI,EAAM,KAAK,GACxC,IAAI,OAAO,CAAG;EAEpB,OAAO,OAAO,UAAU,CAAC,KAAK,IAAI,IAAI,IAAI;CAC5C,GAEM,KAAa,GAAa,MAAkD;EAChF,IAAI,MAAU,KAAA,GAAW;EAEzB,IAAM,IAAM,MAAM,QAAQ,CAAK,IAAI,EAAM,KAAK;EAE9C,IAAI;GACF,OAAO,KAAK,MAAM,CAAG;EACvB,QAAQ;GACN,IAAI,GAAQ,MAAM,IAAI,EAAe,wBAAwB,EAAI,KAAK,GAAK;GAE3E;EACF;CACF,GAEM,IAAW,EAAI,OACf,IAAU,EAAI,MACd,IAAY,EAAI,QAChB,IAAY,EAAI,QAChB,IAAU,EAAI,MAEd,IAAS,MAAc,KAAA,IAA6C,KAAA,IAAjC,EAAU,UAAU,CAAS,GAChE,IAAO,MAAY,KAAA,IAAyC,KAAA,IAA7B,EAAU,QAAQ,CAAO;CAU9D,OAAO;EAPL,GAAI,MAAW,KAAA,KAAa,EAAE,UAAO;EACrC,GAAI,MAAS,KAAA,KAAa,EAAE,QAAK;EACjC,GAAI,MAAc,KAAA,KAAa,EAAE,QAAQ,MAAM,QAAQ,CAAS,IAAI,EAAU,KAAK,EAAU;EAC7F,OAAO,EAAiB,GAAU,CAAY;EAC9C,MAAM,EAAiB,GAAS,CAAC;CAG5B;AACT"}
package/dist/core.cjs ADDED
@@ -0,0 +1,2 @@
1
+ const e=require("./errors.cjs");function t(t){let n=new Set,r=new Set,i=new AbortController,a,o=!1;return{cancelTimer(){a&&=(clearTimeout(a),void 0)},get disposalSignal(){return i.signal},dispose(){a&&=(clearTimeout(a),void 0);for(let t of r)t.timeoutId!==void 0&&clearTimeout(t.timeoutId),t.reject(new e.SourceDisposedError);r.clear(),n.clear(),o=!0,i.abort()},flush(e){return a?(clearTimeout(a),a=void 0,e()):Promise.resolve()},get isDisposed(){return o},get isScheduled(){return a!==void 0},notify(e){if(!o){(e??t?.onBeforeNotify)?.();for(let e of n)e();for(let e of r)e.check()}},ready(t,n){return o?Promise.reject(new e.SourceDisposedError):t()?Promise.resolve():new Promise((i,a)=>{let o={check:()=>{t()&&(o.timeoutId!==void 0&&clearTimeout(o.timeoutId),r.delete(o),i())},reject:a};n!==void 0&&(o.timeoutId=setTimeout(()=>{r.delete(o),a(new e.SourceTimeoutError(n))},n)),r.add(o)})},schedule(e,t){o||(a&&clearTimeout(a),a=setTimeout(()=>{a=void 0,e()},t))},subscribe(e){return o?()=>{}:(n.add(e),()=>n.delete(e))}}}exports.createSourceCore=t;
2
+ //# sourceMappingURL=core.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.cjs","names":[],"sources":["../src/core.ts"],"sourcesContent":["import { SourceDisposedError, SourceTimeoutError } from './errors';\n\n/**\n * Shared source infrastructure: listener management, debounce scheduling, and ready() polling.\n * All source factories compose this core rather than duplicating the same patterns.\n */\nexport type SourceCore = {\n /** Cancels any pending timer without invoking the callback. */\n cancelTimer(): void;\n\n /** `AbortSignal` that is aborted when `dispose()` is called. */\n readonly disposalSignal: AbortSignal;\n\n /**\n * Clears all listeners, cancels any pending timer, and marks the core as disposed.\n * Subsequent method calls on the core become no-ops.\n */\n dispose(): void;\n\n /**\n * If a timer is scheduled, cancels it and immediately invokes `fn`.\n * Returns a resolved Promise when no timer is pending.\n */\n flush(fn: () => Promise<void>): Promise<void>;\n\n /** Whether `dispose()` has been called. All methods become no-ops after disposal. */\n readonly isDisposed: boolean;\n\n /** Whether a debounce timer is currently scheduled (drives `meta.isSearchPending`). */\n readonly isScheduled: boolean;\n\n /**\n * Fire all registered listeners and wake any `ready()` waiters.\n * When provided, `onBefore` runs once before listeners are called — use it to\n * refresh cached meta/current before subscribers observe the new state.\n */\n notify(onBefore?: () => void): void;\n\n /**\n * Returns a Promise that resolves when `isIdle()` returns true.\n * Resolves synchronously if already idle.\n * Rejects with a `TimeoutError` after `timeoutMs` ms if still not idle.\n */\n ready(isIdle: () => boolean, timeoutMs?: number): Promise<void>;\n\n /**\n * Schedules a debounced callback, replacing any pending timer.\n * The callback is invoked after `delayMs` ms of inactivity.\n */\n schedule(fn: () => void, delayMs: number): void;\n\n /** Subscribe to change notifications. Returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n};\n\nexport function createSourceCore(opts?: { onBeforeNotify?: () => void }): SourceCore {\n const listeners = new Set<() => void>();\n const readyWaiters = new Set<{\n check: () => void;\n reject: (err: unknown) => void;\n timeoutId?: ReturnType<typeof setTimeout>;\n }>();\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n let disposed = false;\n\n return {\n cancelTimer() {\n if (timer) {\n clearTimeout(timer);\n timer = undefined;\n }\n },\n\n get disposalSignal() {\n return controller.signal;\n },\n\n dispose() {\n if (timer) {\n clearTimeout(timer);\n timer = undefined;\n }\n\n for (const waiter of readyWaiters) {\n if (waiter.timeoutId !== undefined) clearTimeout(waiter.timeoutId);\n\n waiter.reject(new SourceDisposedError());\n }\n\n readyWaiters.clear();\n listeners.clear();\n disposed = true;\n controller.abort();\n },\n\n flush(fn) {\n if (!timer) return Promise.resolve();\n\n clearTimeout(timer);\n timer = undefined;\n\n return fn();\n },\n\n get isDisposed() {\n return disposed;\n },\n\n get isScheduled() {\n return timer !== undefined;\n },\n\n notify(onBefore?: () => void) {\n if (disposed) return;\n\n (onBefore ?? opts?.onBeforeNotify)?.();\n\n for (const listener of listeners) {\n listener();\n }\n\n for (const waiter of readyWaiters) {\n waiter.check();\n }\n },\n\n ready(isIdle, timeoutMs) {\n if (disposed) return Promise.reject(new SourceDisposedError());\n\n if (isIdle()) return Promise.resolve();\n\n return new Promise<void>((resolve, reject) => {\n const waiter: { check: () => void; reject: (err: unknown) => void; timeoutId?: ReturnType<typeof setTimeout> } =\n {\n check: () => {\n if (isIdle()) {\n if (waiter.timeoutId !== undefined) clearTimeout(waiter.timeoutId);\n\n readyWaiters.delete(waiter);\n resolve();\n }\n },\n reject,\n };\n\n if (timeoutMs !== undefined) {\n waiter.timeoutId = setTimeout(() => {\n readyWaiters.delete(waiter);\n reject(new SourceTimeoutError(timeoutMs));\n }, timeoutMs);\n }\n\n readyWaiters.add(waiter);\n });\n },\n\n schedule(fn, delayMs) {\n if (disposed) return;\n\n if (timer) clearTimeout(timer);\n\n timer = setTimeout(() => {\n timer = undefined;\n fn();\n }, delayMs);\n },\n\n subscribe(listener) {\n if (disposed) return () => {};\n\n listeners.add(listener);\n\n return () => listeners.delete(listener);\n },\n };\n}\n"],"mappings":"gCAuDA,SAAgB,EAAiB,EAAoD,CACnF,IAAM,EAAY,IAAI,IAChB,EAAe,IAAI,IAKnB,EAAa,IAAI,gBACnB,EACA,EAAW,GAEf,MAAO,CACL,aAAc,CACZ,AAEE,KADA,aAAa,CAAK,EACV,IAAA,GAEZ,EAEA,IAAI,gBAAiB,CACnB,OAAO,EAAW,MACpB,EAEA,SAAU,CACR,AAEE,KADA,aAAa,CAAK,EACV,IAAA,IAGV,IAAK,IAAM,KAAU,EACf,EAAO,YAAc,IAAA,IAAW,aAAa,EAAO,SAAS,EAEjE,EAAO,OAAO,IAAI,EAAA,mBAAqB,EAGzC,EAAa,MAAM,EACnB,EAAU,MAAM,EAChB,EAAW,GACX,EAAW,MAAM,CACnB,EAEA,MAAM,EAAI,CAMR,OALK,GAEL,aAAa,CAAK,EAClB,EAAQ,IAAA,GAED,EAAG,GALS,QAAQ,QAAQ,CAMrC,EAEA,IAAI,YAAa,CACf,OAAO,CACT,EAEA,IAAI,aAAc,CAChB,OAAO,IAAU,IAAA,EACnB,EAEA,OAAO,EAAuB,CACxB,MAEJ,EAAC,GAAY,GAAM,eAAA,GAAkB,EAErC,IAAK,IAAM,KAAY,EACrB,EAAS,EAGX,IAAK,IAAM,KAAU,EACnB,EAAO,MAAM,CAPsB,CASvC,EAEA,MAAM,EAAQ,EAAW,CAKvB,OAJI,EAAiB,QAAQ,OAAO,IAAI,EAAA,mBAAqB,EAEzD,EAAO,EAAU,QAAQ,QAAQ,EAE9B,IAAI,SAAe,EAAS,IAAW,CAC5C,IAAM,EACJ,CACE,UAAa,CACP,EAAO,IACL,EAAO,YAAc,IAAA,IAAW,aAAa,EAAO,SAAS,EAEjE,EAAa,OAAO,CAAM,EAC1B,EAAQ,EAEZ,EACA,QACF,EAEE,IAAc,IAAA,KAChB,EAAO,UAAY,eAAiB,CAClC,EAAa,OAAO,CAAM,EAC1B,EAAO,IAAI,EAAA,mBAAmB,CAAS,CAAC,CAC1C,EAAG,CAAS,GAGd,EAAa,IAAI,CAAM,CACzB,CAAC,CACH,EAEA,SAAS,EAAI,EAAS,CAChB,IAEA,GAAO,aAAa,CAAK,EAE7B,EAAQ,eAAiB,CACvB,EAAQ,IAAA,GACR,EAAG,CACL,EAAG,CAAO,EACZ,EAEA,UAAU,EAAU,CAKlB,OAJI,MAAuB,CAAC,GAE5B,EAAU,IAAI,CAAQ,MAET,EAAU,OAAO,CAAQ,EACxC,CACF,CACF"}
package/dist/core.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared source infrastructure: listener management, debounce scheduling, and ready() polling.
3
+ * All source factories compose this core rather than duplicating the same patterns.
4
+ */
5
+ export type SourceCore = {
6
+ /** Cancels any pending timer without invoking the callback. */
7
+ cancelTimer(): void;
8
+ /** `AbortSignal` that is aborted when `dispose()` is called. */
9
+ readonly disposalSignal: AbortSignal;
10
+ /**
11
+ * Clears all listeners, cancels any pending timer, and marks the core as disposed.
12
+ * Subsequent method calls on the core become no-ops.
13
+ */
14
+ dispose(): void;
15
+ /**
16
+ * If a timer is scheduled, cancels it and immediately invokes `fn`.
17
+ * Returns a resolved Promise when no timer is pending.
18
+ */
19
+ flush(fn: () => Promise<void>): Promise<void>;
20
+ /** Whether `dispose()` has been called. All methods become no-ops after disposal. */
21
+ readonly isDisposed: boolean;
22
+ /** Whether a debounce timer is currently scheduled (drives `meta.isSearchPending`). */
23
+ readonly isScheduled: boolean;
24
+ /**
25
+ * Fire all registered listeners and wake any `ready()` waiters.
26
+ * When provided, `onBefore` runs once before listeners are called — use it to
27
+ * refresh cached meta/current before subscribers observe the new state.
28
+ */
29
+ notify(onBefore?: () => void): void;
30
+ /**
31
+ * Returns a Promise that resolves when `isIdle()` returns true.
32
+ * Resolves synchronously if already idle.
33
+ * Rejects with a `TimeoutError` after `timeoutMs` ms if still not idle.
34
+ */
35
+ ready(isIdle: () => boolean, timeoutMs?: number): Promise<void>;
36
+ /**
37
+ * Schedules a debounced callback, replacing any pending timer.
38
+ * The callback is invoked after `delayMs` ms of inactivity.
39
+ */
40
+ schedule(fn: () => void, delayMs: number): void;
41
+ /** Subscribe to change notifications. Returns an unsubscribe function. */
42
+ subscribe(listener: () => void): () => void;
43
+ };
44
+ export declare function createSourceCore(opts?: {
45
+ onBeforeNotify?: () => void;
46
+ }): SourceCore;
47
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,+DAA+D;IAC/D,WAAW,IAAI,IAAI,CAAC;IAEpB,gEAAgE;IAChE,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC;IAErC;;;OAGG;IACH,OAAO,IAAI,IAAI,CAAC;IAEhB;;;OAGG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9C,qFAAqF;IACrF,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAE7B,uFAAuF;IACvF,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAE9B;;;;OAIG;IACH,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAEpC;;;;OAIG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhE;;;OAGG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAEhD,0EAA0E;IAC1E,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC7C,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,IAAI,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,GAAG,UAAU,CAyHnF"}
package/dist/core.js ADDED
@@ -0,0 +1,59 @@
1
+ import { SourceDisposedError as e, SourceTimeoutError as t } from "./errors.js";
2
+ //#region src/core.ts
3
+ function n(n) {
4
+ let r = /* @__PURE__ */ new Set(), i = /* @__PURE__ */ new Set(), a = new AbortController(), o, s = !1;
5
+ return {
6
+ cancelTimer() {
7
+ o &&= (clearTimeout(o), void 0);
8
+ },
9
+ get disposalSignal() {
10
+ return a.signal;
11
+ },
12
+ dispose() {
13
+ o &&= (clearTimeout(o), void 0);
14
+ for (let t of i) t.timeoutId !== void 0 && clearTimeout(t.timeoutId), t.reject(new e());
15
+ i.clear(), r.clear(), s = !0, a.abort();
16
+ },
17
+ flush(e) {
18
+ return o ? (clearTimeout(o), o = void 0, e()) : Promise.resolve();
19
+ },
20
+ get isDisposed() {
21
+ return s;
22
+ },
23
+ get isScheduled() {
24
+ return o !== void 0;
25
+ },
26
+ notify(e) {
27
+ if (!s) {
28
+ (e ?? n?.onBeforeNotify)?.();
29
+ for (let e of r) e();
30
+ for (let e of i) e.check();
31
+ }
32
+ },
33
+ ready(n, r) {
34
+ return s ? Promise.reject(new e()) : n() ? Promise.resolve() : new Promise((e, a) => {
35
+ let o = {
36
+ check: () => {
37
+ n() && (o.timeoutId !== void 0 && clearTimeout(o.timeoutId), i.delete(o), e());
38
+ },
39
+ reject: a
40
+ };
41
+ r !== void 0 && (o.timeoutId = setTimeout(() => {
42
+ i.delete(o), a(new t(r));
43
+ }, r)), i.add(o);
44
+ });
45
+ },
46
+ schedule(e, t) {
47
+ s || (o && clearTimeout(o), o = setTimeout(() => {
48
+ o = void 0, e();
49
+ }, t));
50
+ },
51
+ subscribe(e) {
52
+ return s ? () => {} : (r.add(e), () => r.delete(e));
53
+ }
54
+ };
55
+ }
56
+ //#endregion
57
+ export { n as createSourceCore };
58
+
59
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","names":[],"sources":["../src/core.ts"],"sourcesContent":["import { SourceDisposedError, SourceTimeoutError } from './errors';\n\n/**\n * Shared source infrastructure: listener management, debounce scheduling, and ready() polling.\n * All source factories compose this core rather than duplicating the same patterns.\n */\nexport type SourceCore = {\n /** Cancels any pending timer without invoking the callback. */\n cancelTimer(): void;\n\n /** `AbortSignal` that is aborted when `dispose()` is called. */\n readonly disposalSignal: AbortSignal;\n\n /**\n * Clears all listeners, cancels any pending timer, and marks the core as disposed.\n * Subsequent method calls on the core become no-ops.\n */\n dispose(): void;\n\n /**\n * If a timer is scheduled, cancels it and immediately invokes `fn`.\n * Returns a resolved Promise when no timer is pending.\n */\n flush(fn: () => Promise<void>): Promise<void>;\n\n /** Whether `dispose()` has been called. All methods become no-ops after disposal. */\n readonly isDisposed: boolean;\n\n /** Whether a debounce timer is currently scheduled (drives `meta.isSearchPending`). */\n readonly isScheduled: boolean;\n\n /**\n * Fire all registered listeners and wake any `ready()` waiters.\n * When provided, `onBefore` runs once before listeners are called — use it to\n * refresh cached meta/current before subscribers observe the new state.\n */\n notify(onBefore?: () => void): void;\n\n /**\n * Returns a Promise that resolves when `isIdle()` returns true.\n * Resolves synchronously if already idle.\n * Rejects with a `TimeoutError` after `timeoutMs` ms if still not idle.\n */\n ready(isIdle: () => boolean, timeoutMs?: number): Promise<void>;\n\n /**\n * Schedules a debounced callback, replacing any pending timer.\n * The callback is invoked after `delayMs` ms of inactivity.\n */\n schedule(fn: () => void, delayMs: number): void;\n\n /** Subscribe to change notifications. Returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n};\n\nexport function createSourceCore(opts?: { onBeforeNotify?: () => void }): SourceCore {\n const listeners = new Set<() => void>();\n const readyWaiters = new Set<{\n check: () => void;\n reject: (err: unknown) => void;\n timeoutId?: ReturnType<typeof setTimeout>;\n }>();\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n let disposed = false;\n\n return {\n cancelTimer() {\n if (timer) {\n clearTimeout(timer);\n timer = undefined;\n }\n },\n\n get disposalSignal() {\n return controller.signal;\n },\n\n dispose() {\n if (timer) {\n clearTimeout(timer);\n timer = undefined;\n }\n\n for (const waiter of readyWaiters) {\n if (waiter.timeoutId !== undefined) clearTimeout(waiter.timeoutId);\n\n waiter.reject(new SourceDisposedError());\n }\n\n readyWaiters.clear();\n listeners.clear();\n disposed = true;\n controller.abort();\n },\n\n flush(fn) {\n if (!timer) return Promise.resolve();\n\n clearTimeout(timer);\n timer = undefined;\n\n return fn();\n },\n\n get isDisposed() {\n return disposed;\n },\n\n get isScheduled() {\n return timer !== undefined;\n },\n\n notify(onBefore?: () => void) {\n if (disposed) return;\n\n (onBefore ?? opts?.onBeforeNotify)?.();\n\n for (const listener of listeners) {\n listener();\n }\n\n for (const waiter of readyWaiters) {\n waiter.check();\n }\n },\n\n ready(isIdle, timeoutMs) {\n if (disposed) return Promise.reject(new SourceDisposedError());\n\n if (isIdle()) return Promise.resolve();\n\n return new Promise<void>((resolve, reject) => {\n const waiter: { check: () => void; reject: (err: unknown) => void; timeoutId?: ReturnType<typeof setTimeout> } =\n {\n check: () => {\n if (isIdle()) {\n if (waiter.timeoutId !== undefined) clearTimeout(waiter.timeoutId);\n\n readyWaiters.delete(waiter);\n resolve();\n }\n },\n reject,\n };\n\n if (timeoutMs !== undefined) {\n waiter.timeoutId = setTimeout(() => {\n readyWaiters.delete(waiter);\n reject(new SourceTimeoutError(timeoutMs));\n }, timeoutMs);\n }\n\n readyWaiters.add(waiter);\n });\n },\n\n schedule(fn, delayMs) {\n if (disposed) return;\n\n if (timer) clearTimeout(timer);\n\n timer = setTimeout(() => {\n timer = undefined;\n fn();\n }, delayMs);\n },\n\n subscribe(listener) {\n if (disposed) return () => {};\n\n listeners.add(listener);\n\n return () => listeners.delete(listener);\n },\n };\n}\n"],"mappings":";;AAuDA,SAAgB,EAAiB,GAAoD;CACnF,IAAM,oBAAY,IAAI,IAAgB,GAChC,oBAAe,IAAI,IAItB,GACG,IAAa,IAAI,gBAAgB,GACnC,GACA,IAAW;CAEf,OAAO;EACL,cAAc;GACZ,AAEE,OADA,aAAa,CAAK,GACV,KAAA;EAEZ;EAEA,IAAI,iBAAiB;GACnB,OAAO,EAAW;EACpB;EAEA,UAAU;GACR,AAEE,OADA,aAAa,CAAK,GACV,KAAA;GAGV,KAAK,IAAM,KAAU,GAGnB,AAFI,EAAO,cAAc,KAAA,KAAW,aAAa,EAAO,SAAS,GAEjE,EAAO,OAAO,IAAI,EAAoB,CAAC;GAMzC,AAHA,EAAa,MAAM,GACnB,EAAU,MAAM,GAChB,IAAW,IACX,EAAW,MAAM;EACnB;EAEA,MAAM,GAAI;GAMR,OALK,KAEL,aAAa,CAAK,GAClB,IAAQ,KAAA,GAED,EAAG,KALS,QAAQ,QAAQ;EAMrC;EAEA,IAAI,aAAa;GACf,OAAO;EACT;EAEA,IAAI,cAAc;GAChB,OAAO,MAAU,KAAA;EACnB;EAEA,OAAO,GAAuB;GACxB,QAEJ;KAAC,KAAY,GAAM,eAAA,GAAkB;IAErC,KAAK,IAAM,KAAY,GACrB,EAAS;IAGX,KAAK,IAAM,KAAU,GACnB,EAAO,MAAM;GAPsB;EASvC;EAEA,MAAM,GAAQ,GAAW;GAKvB,OAJI,IAAiB,QAAQ,OAAO,IAAI,EAAoB,CAAC,IAEzD,EAAO,IAAU,QAAQ,QAAQ,IAE9B,IAAI,SAAe,GAAS,MAAW;IAC5C,IAAM,IACJ;KACE,aAAa;MACX,AAAI,EAAO,MACL,EAAO,cAAc,KAAA,KAAW,aAAa,EAAO,SAAS,GAEjE,EAAa,OAAO,CAAM,GAC1B,EAAQ;KAEZ;KACA;IACF;IASF,AAPI,MAAc,KAAA,MAChB,EAAO,YAAY,iBAAiB;KAElC,AADA,EAAa,OAAO,CAAM,GAC1B,EAAO,IAAI,EAAmB,CAAS,CAAC;IAC1C,GAAG,CAAS,IAGd,EAAa,IAAI,CAAM;GACzB,CAAC;EACH;EAEA,SAAS,GAAI,GAAS;GAChB,MAEA,KAAO,aAAa,CAAK,GAE7B,IAAQ,iBAAiB;IAEvB,AADA,IAAQ,KAAA,GACR,EAAG;GACL,GAAG,CAAO;EACZ;EAEA,UAAU,GAAU;GAKlB,OAJI,UAAuB,CAAC,KAE5B,EAAU,IAAI,CAAQ,SAET,EAAU,OAAO,CAAQ;EACxC;CACF;AACF"}
@@ -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,l,u,d,f=[],p=0,m=null,h=null,g=[],_={error:null,hasNextPage:!1,hasPrevPage:!1,isLoading:!1,isSearchPending:!1,pageSize:o,totalItems:0},v=()=>{g=f,_={error:m,hasNextPage:u!==void 0,hasPrevPage:d!==void 0,isLoading:y.pendingCount()>0,isSearchPending:y.core.isScheduled,pageSize:o,totalItems:p}},y=n.createAsyncSource(i,a,v),{autoFetch:b,debounceMs:x,retryAttempts:S,retryDelay:C}=y;v();let w=()=>{y.disposed||y.core.notify()},T=()=>{h&&=(h.resolve(),null)},E=()=>({...c!==void 0&&{after:c},...l!==void 0&&{before:l},...s&&{search:s},limit:o}),D=n=>(m=null,y.fetch(n,async(n,a,o)=>{let s=Date.now();try{let e=await(0,r.retry)(e=>i.fetch(n,e),{delay:C,signal:a,times:S+1});o()&&(f=e.items,p=e.total??e.items.length,u=e.nextCursor,d=e.prevCursor,m=null,i.onFetch?.({durationMs:Date.now()-s,query:n,status:`success`}))}catch(r){if(a.aborted)return;o()&&(f=[],p=0,u=void 0,d=void 0,m=new e.SourcererError(t.extractError(r),{cause:r,context:{kind:`cursor`,limit:n.limit,...n.search&&{search:n.search}}}),i.onFetch?.({durationMs:Date.now()-s,error:m,query:n,status:`error`}))}},()=>{w(),y.pendingCount()===0&&T()})),O=()=>D(E());return b&&O(),y.startRefreshInterval(()=>void O()),{get current(){return g},get disposalSignal(){return y.disposalSignal},dispose:()=>{T(),y.dispose()},get disposed(){return y.disposed},get meta(){return _},next(){return u?(c=u,l=void 0,O()):Promise.resolve()},patch(e){let t=!1;if(e.limit!==void 0){let n=Math.max(1,Math.trunc(e.limit));n!==o&&(o=n,c=void 0,l=void 0,t=!0)}return`search`in e&&(e.search!==s||y.core.isScheduled)&&(s=e.search??``,c=void 0,l=void 0,t=!0),t?(y.core.cancelTimer(),T(),O()):Promise.resolve()},prev(){return d?(l=d,c=void 0,O()):Promise.resolve()},get query(){return{...c!==void 0&&{after:c},...l!==void 0&&{before:l},...s&&{search:s},limit:o}},ready(e){return y.ready(e)},refresh(){return O()},reset(){return y.core.cancelTimer(),T(),s=``,c=void 0,l=void 0,O()},search(e,t){if(t?.immediate)return e===s&&!y.core.isScheduled?Promise.resolve():(y.core.cancelTimer(),T(),s=e,c=void 0,l=void 0,O());if(e===s)return Promise.resolve();T(),s=e,c=void 0,l=void 0;let n,r=new Promise(e=>{n=e});return h={promise:r,resolve:n},y.core.schedule(()=>void O(),x),w(),r},subscribe:e=>y.core.subscribe(e),[Symbol.dispose]:()=>{T(),y.dispose()}}}exports.createCursorSource=i;
2
+ //# sourceMappingURL=cursorSource.cjs.map