@vielzeug/sourcerer 1.0.5 → 1.1.0
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/dist/_dev.cjs +2 -0
- package/dist/_dev.cjs.map +1 -0
- package/dist/_dev.d.ts +2 -0
- package/dist/_dev.d.ts.map +1 -0
- package/dist/_dev.js +12 -0
- package/dist/_dev.js.map +1 -0
- package/dist/_utils.cjs +1 -1
- package/dist/_utils.cjs.map +1 -1
- package/dist/_utils.d.ts +20 -0
- package/dist/_utils.d.ts.map +1 -1
- package/dist/_utils.js +16 -3
- package/dist/_utils.js.map +1 -1
- package/dist/codecs.cjs +1 -1
- package/dist/codecs.cjs.map +1 -1
- package/dist/codecs.d.ts +1 -1
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +21 -19
- package/dist/codecs.js.map +1 -1
- package/dist/core.cjs +1 -1
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.ts +25 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/dist/cursorSource.cjs +1 -1
- package/dist/cursorSource.cjs.map +1 -1
- package/dist/cursorSource.js +69 -69
- package/dist/cursorSource.js.map +1 -1
- package/dist/derive.cjs +1 -1
- package/dist/derive.cjs.map +1 -1
- package/dist/derive.d.ts +1 -0
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +2 -4
- package/dist/derive.js.map +1 -1
- package/dist/devtools.cjs +2 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.ts +52 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +20 -0
- package/dist/devtools.js.map +1 -0
- package/dist/errors.cjs +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fetchManager.cjs.map +1 -1
- package/dist/fetchManager.d.ts +13 -0
- package/dist/fetchManager.d.ts.map +1 -1
- package/dist/fetchManager.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -14
- package/dist/infiniteSource.cjs +1 -1
- package/dist/infiniteSource.cjs.map +1 -1
- package/dist/infiniteSource.js +62 -62
- package/dist/infiniteSource.js.map +1 -1
- package/dist/localSource.cjs +1 -1
- package/dist/localSource.cjs.map +1 -1
- package/dist/localSource.d.ts.map +1 -1
- package/dist/localSource.js +69 -79
- package/dist/localSource.js.map +1 -1
- package/dist/merge.cjs +1 -1
- package/dist/merge.cjs.map +1 -1
- package/dist/merge.d.ts +6 -1
- package/dist/merge.d.ts.map +1 -1
- package/dist/merge.js +8 -1
- package/dist/merge.js.map +1 -1
- package/dist/remoteSource.cjs +1 -1
- package/dist/remoteSource.cjs.map +1 -1
- package/dist/remoteSource.d.ts.map +1 -1
- package/dist/remoteSource.js +94 -94
- package/dist/remoteSource.js.map +1 -1
- package/dist/sourcerer.cjs +1 -1
- package/dist/sourcerer.cjs.map +1 -1
- package/dist/sourcerer.iife.js +1 -1
- package/dist/sourcerer.iife.js.map +1 -1
- package/dist/sourcerer.js +1 -1
- package/dist/sourcerer.js.map +1 -1
- package/dist/types.d.ts +14 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -3
- package/dist/applyQuery.cjs +0 -2
- package/dist/applyQuery.cjs.map +0 -1
- package/dist/applyQuery.d.ts +0 -20
- package/dist/applyQuery.d.ts.map +0 -1
- package/dist/applyQuery.js +0 -8
- package/dist/applyQuery.js.map +0 -1
package/dist/_dev.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dev.cjs","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __SOURCERER_PROD__?: boolean }).__SOURCERER_PROD__;\n\n/** @internal */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/sourcerer] ${msg}`);\n}\n\n/** @internal — Run fn only in dev builds. Use when dev-only logic goes beyond a single warn() call. */\nexport function devOnly(fn: () => void): void {\n if (isDev) fn();\n}\n"],"mappings":"AAAA,IAAM,EAAQ,CAAE,WAAgD,mBAGhE,SAAgB,EAAK,EAAmB,CAClC,GAAO,QAAQ,KAAK,yBAAyB,GAAK,CACxD,CAGA,SAAgB,EAAQ,EAAsB,CACxC,GAAO,EAAG,CAChB"}
|
package/dist/_dev.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dev.d.ts","sourceRoot":"","sources":["../src/_dev.ts"],"names":[],"mappings":""}
|
package/dist/_dev.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/_dev.ts
|
|
2
|
+
var e = !globalThis.__SOURCERER_PROD__;
|
|
3
|
+
function t(t) {
|
|
4
|
+
e && console.warn(`[@vielzeug/sourcerer] ${t}`);
|
|
5
|
+
}
|
|
6
|
+
function n(t) {
|
|
7
|
+
e && t();
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { n as devOnly, t as warn };
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=_dev.js.map
|
package/dist/_dev.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dev.js","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __SOURCERER_PROD__?: boolean }).__SOURCERER_PROD__;\n\n/** @internal */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/sourcerer] ${msg}`);\n}\n\n/** @internal — Run fn only in dev builds. Use when dev-only logic goes beyond a single warn() call. */\nexport function devOnly(fn: () => void): void {\n if (isDev) fn();\n}\n"],"mappings":";AAAA,IAAM,IAAQ,CAAE,WAAgD;AAGhE,SAAgB,EAAK,GAAmB;CACtC,AAAI,KAAO,QAAQ,KAAK,yBAAyB,GAAK;AACxD;AAGA,SAAgB,EAAQ,GAAsB;CAC5C,AAAI,KAAO,EAAG;AAChB"}
|
package/dist/_utils.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
const e=require("./_dev.cjs");let t=require("@vielzeug/arsenal");var n=t.hash,r=e=>e instanceof Error?e.message:typeof e==`string`&&e.length>0?e:`Request failed`,i=e=>(0,t.backoff)(e);function a(e,t=200){return e.replace(/\p{C}/gu,`?`).slice(0,t)}var o=new Set;function s(t,n,r){let i=Number.isFinite(t)?Math.max(1,Math.trunc(t)):1;if(!Number.isFinite(t)||t<1){let a=`${n}:${r}`;o.has(a)||(o.add(a),e.devOnly(()=>e.warn(`${n}: ${r} ${t} is invalid — clamped to ${i}. Must be a positive integer.`)))}return i}exports.clampPositiveInt=s,exports.defaultKeyOf=n,exports.defaultRetryDelay=i,exports.extractError=r,exports.sanitizeForLog=a;
|
|
2
2
|
//# sourceMappingURL=_utils.cjs.map
|
package/dist/_utils.cjs.map
CHANGED
|
@@ -1 +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":"
|
|
1
|
+
{"version":3,"file":"_utils.cjs","names":[],"sources":["../src/_utils.ts"],"sourcesContent":["import { backoff, hash, retry } from '@vielzeug/arsenal';\n\nimport { devOnly, warn } from './_dev';\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\n/**\n * Strips control characters (which could otherwise inject terminal escape sequences into\n * `console.warn`/`console.debug` output) and caps length before interpolating caller- or\n * user-controlled text into a dev warning. Same approach as `@vielzeug/forge`'s\n * `sanitizeForLog` — duplicated rather than shared, per this repo's zero-cross-package-utility\n * convention for internal-only helpers.\n */\nexport function sanitizeForLog(text: string, maxLength = 200): string {\n return text.replace(/\\p{C}/gu, '?').slice(0, maxLength);\n}\n\nconst warnedClamps = new Set<string>();\n\n/**\n * Coerces a user-supplied `limit`/`page`/target value to a positive integer, warning once per\n * `(apiLabel, fieldName)` call site in dev when the raw input needed clamping (negative, zero,\n * or non-finite) — every source factory previously did `Math.max(1, Math.trunc(value))`\n * silently, so a typo'd or miscalculated config value (e.g. `limit: -1`) produced no signal at\n * all. The once-per-call-site de-dup matters here specifically because `limit`/`page` reach this\n * function via `patch()`, which realistically sits behind live UI (a numeric input, a stepper) —\n * without it, a single persistently-invalid bound value would re-warn on every keystroke.\n * Fractional inputs (e.g. `2.5`) are truncated without a warning — that's a harmless\n * normalization, not a likely mistake.\n */\nexport function clampPositiveInt(value: number, apiLabel: string, fieldName: string): number {\n const clamped = Number.isFinite(value) ? Math.max(1, Math.trunc(value)) : 1;\n\n if (!Number.isFinite(value) || value < 1) {\n const key = `${apiLabel}:${fieldName}`;\n\n if (!warnedClamps.has(key)) {\n warnedClamps.add(key);\n devOnly(() =>\n warn(`${apiLabel}: ${fieldName} ${value} is invalid — clamped to ${clamped}. Must be a positive integer.`),\n );\n }\n }\n\n return clamped;\n}\n"],"mappings":"iEAOA,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,EAS7E,SAAgB,EAAe,EAAc,EAAY,IAAa,CACpE,OAAO,EAAK,QAAQ,UAAW,GAAG,CAAC,CAAC,MAAM,EAAG,CAAS,CACxD,CAEA,IAAM,EAAe,IAAI,IAazB,SAAgB,EAAiB,EAAe,EAAkB,EAA2B,CAC3F,IAAM,EAAU,OAAO,SAAS,CAAK,EAAI,KAAK,IAAI,EAAG,KAAK,MAAM,CAAK,CAAC,EAAI,EAE1E,GAAI,CAAC,OAAO,SAAS,CAAK,GAAK,EAAQ,EAAG,CACxC,IAAM,EAAM,GAAG,EAAS,GAAG,IAEtB,EAAa,IAAI,CAAG,IACvB,EAAa,IAAI,CAAG,EACpB,EAAA,YACE,EAAA,KAAK,GAAG,EAAS,IAAI,EAAU,GAAG,EAAM,2BAA2B,EAAQ,8BAA8B,CAC3G,EAEJ,CAEA,OAAO,CACT"}
|
package/dist/_utils.d.ts
CHANGED
|
@@ -5,4 +5,24 @@ export declare const defaultKeyOf: typeof hash;
|
|
|
5
5
|
export declare const extractError: (reason: unknown) => string;
|
|
6
6
|
/** Default exponential backoff: 1 s, 2 s, 4 s, … capped at 30 s. Receives a 0-indexed failure count. */
|
|
7
7
|
export declare const defaultRetryDelay: (attempt: number) => number;
|
|
8
|
+
/**
|
|
9
|
+
* Strips control characters (which could otherwise inject terminal escape sequences into
|
|
10
|
+
* `console.warn`/`console.debug` output) and caps length before interpolating caller- or
|
|
11
|
+
* user-controlled text into a dev warning. Same approach as `@vielzeug/forge`'s
|
|
12
|
+
* `sanitizeForLog` — duplicated rather than shared, per this repo's zero-cross-package-utility
|
|
13
|
+
* convention for internal-only helpers.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sanitizeForLog(text: string, maxLength?: number): string;
|
|
16
|
+
/**
|
|
17
|
+
* Coerces a user-supplied `limit`/`page`/target value to a positive integer, warning once per
|
|
18
|
+
* `(apiLabel, fieldName)` call site in dev when the raw input needed clamping (negative, zero,
|
|
19
|
+
* or non-finite) — every source factory previously did `Math.max(1, Math.trunc(value))`
|
|
20
|
+
* silently, so a typo'd or miscalculated config value (e.g. `limit: -1`) produced no signal at
|
|
21
|
+
* all. The once-per-call-site de-dup matters here specifically because `limit`/`page` reach this
|
|
22
|
+
* function via `patch()`, which realistically sits behind live UI (a numeric input, a stepper) —
|
|
23
|
+
* without it, a single persistently-invalid bound value would re-warn on every keystroke.
|
|
24
|
+
* Fractional inputs (e.g. `2.5`) are truncated without a warning — that's a harmless
|
|
25
|
+
* normalization, not a likely mistake.
|
|
26
|
+
*/
|
|
27
|
+
export declare function clampPositiveInt(value: number, apiLabel: string, fieldName: string): number;
|
|
8
28
|
//# sourceMappingURL=_utils.d.ts.map
|
package/dist/_utils.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"_utils.d.ts","sourceRoot":"","sources":["../src/_utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,IAAI,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAIzD,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;AAE/E;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,MAAM,CAEpE;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAe3F"}
|
package/dist/_utils.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { devOnly as e, warn as t } from "./_dev.js";
|
|
2
|
+
import { backoff as n, hash as r, retry as i } from "@vielzeug/arsenal";
|
|
2
3
|
//#region src/_utils.ts
|
|
3
|
-
var
|
|
4
|
+
var a = r, o = (e) => e instanceof Error ? e.message : typeof e == "string" && e.length > 0 ? e : "Request failed", s = (e) => n(e);
|
|
5
|
+
function c(e, t = 200) {
|
|
6
|
+
return e.replace(/\p{C}/gu, "?").slice(0, t);
|
|
7
|
+
}
|
|
8
|
+
var l = /* @__PURE__ */ new Set();
|
|
9
|
+
function u(n, r, i) {
|
|
10
|
+
let a = Number.isFinite(n) ? Math.max(1, Math.trunc(n)) : 1;
|
|
11
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
12
|
+
let o = `${r}:${i}`;
|
|
13
|
+
l.has(o) || (l.add(o), e(() => t(`${r}: ${i} ${n} is invalid — clamped to ${a}. Must be a positive integer.`)));
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
}
|
|
4
17
|
//#endregion
|
|
5
|
-
export {
|
|
18
|
+
export { u as clampPositiveInt, a as defaultKeyOf, s as defaultRetryDelay, o as extractError, i as retry, c as sanitizeForLog };
|
|
6
19
|
|
|
7
20
|
//# sourceMappingURL=_utils.js.map
|
package/dist/_utils.js.map
CHANGED
|
@@ -1 +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":"
|
|
1
|
+
{"version":3,"file":"_utils.js","names":[],"sources":["../src/_utils.ts"],"sourcesContent":["import { backoff, hash, retry } from '@vielzeug/arsenal';\n\nimport { devOnly, warn } from './_dev';\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\n/**\n * Strips control characters (which could otherwise inject terminal escape sequences into\n * `console.warn`/`console.debug` output) and caps length before interpolating caller- or\n * user-controlled text into a dev warning. Same approach as `@vielzeug/forge`'s\n * `sanitizeForLog` — duplicated rather than shared, per this repo's zero-cross-package-utility\n * convention for internal-only helpers.\n */\nexport function sanitizeForLog(text: string, maxLength = 200): string {\n return text.replace(/\\p{C}/gu, '?').slice(0, maxLength);\n}\n\nconst warnedClamps = new Set<string>();\n\n/**\n * Coerces a user-supplied `limit`/`page`/target value to a positive integer, warning once per\n * `(apiLabel, fieldName)` call site in dev when the raw input needed clamping (negative, zero,\n * or non-finite) — every source factory previously did `Math.max(1, Math.trunc(value))`\n * silently, so a typo'd or miscalculated config value (e.g. `limit: -1`) produced no signal at\n * all. The once-per-call-site de-dup matters here specifically because `limit`/`page` reach this\n * function via `patch()`, which realistically sits behind live UI (a numeric input, a stepper) —\n * without it, a single persistently-invalid bound value would re-warn on every keystroke.\n * Fractional inputs (e.g. `2.5`) are truncated without a warning — that's a harmless\n * normalization, not a likely mistake.\n */\nexport function clampPositiveInt(value: number, apiLabel: string, fieldName: string): number {\n const clamped = Number.isFinite(value) ? Math.max(1, Math.trunc(value)) : 1;\n\n if (!Number.isFinite(value) || value < 1) {\n const key = `${apiLabel}:${fieldName}`;\n\n if (!warnedClamps.has(key)) {\n warnedClamps.add(key);\n devOnly(() =>\n warn(`${apiLabel}: ${fieldName} ${value} is invalid — clamped to ${clamped}. Must be a positive integer.`),\n );\n }\n }\n\n return clamped;\n}\n"],"mappings":";;;AAOA,IAAa,IAAe,GAGf,KAAgB,MACvB,aAAkB,QAAc,EAAO,UAEvC,OAAO,KAAW,YAAY,EAAO,SAAS,IAAU,IAErD,kBAII,KAAqB,MAA4B,EAAQ,CAAO;AAS7E,SAAgB,EAAe,GAAc,IAAY,KAAa;CACpE,OAAO,EAAK,QAAQ,WAAW,GAAG,CAAC,CAAC,MAAM,GAAG,CAAS;AACxD;AAEA,IAAM,oBAAe,IAAI,IAAY;AAarC,SAAgB,EAAiB,GAAe,GAAkB,GAA2B;CAC3F,IAAM,IAAU,OAAO,SAAS,CAAK,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,CAAK,CAAC,IAAI;CAE1E,IAAI,CAAC,OAAO,SAAS,CAAK,KAAK,IAAQ,GAAG;EACxC,IAAM,IAAM,GAAG,EAAS,GAAG;EAE3B,AAAK,EAAa,IAAI,CAAG,MACvB,EAAa,IAAI,CAAG,GACpB,QACE,EAAK,GAAG,EAAS,IAAI,EAAU,GAAG,EAAM,2BAA2B,EAAQ,8BAA8B,CAC3G;CAEJ;CAEA,OAAO;AACT"}
|
package/dist/codecs.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./errors.cjs");var
|
|
1
|
+
const e=require("./_dev.cjs"),t=require("./_utils.cjs"),n=require("./errors.cjs");var r=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},i=(r,i={})=>{let a=r instanceof URLSearchParams?Object.fromEntries(r.entries()):r,{defaultLimit:o=20,strict:s=!1}=i,c=(n,r,i)=>{if(r===void 0)return i;let a=Array.isArray(r)?r[0]:r,o=Number(a);return Number.isInteger(o)&&o>0?o:(e.devOnly(()=>e.warn(`decodeQuery: ${n} "${t.sanitizeForLog(a,80)}" is not a positive integer — using ${i}.`)),i)},l=(e,r)=>{if(r===void 0)return;let i=Array.isArray(r)?r[0]:r;try{return JSON.parse(i)}catch{if(s)throw new n.SourcererError(`Invalid query param "${e}": ${t.sanitizeForLog(i,80)}`);return}},u=a.limit,d=a.page,f=a.search,p=a.filter,m=a.sort,h=p===void 0?void 0:l(`filter`,p),g=m===void 0?void 0:l(`sort`,m);return{...h!==void 0&&{filter:h},...g!==void 0&&{sort:g},...f!==void 0&&{search:Array.isArray(f)?f[0]:f},limit:c(`limit`,u,o),page:c(`page`,d,1)}};exports.decodeQuery=i,exports.encodeQuery=r;
|
|
2
2
|
//# sourceMappingURL=codecs.cjs.map
|
package/dist/codecs.cjs.map
CHANGED
|
@@ -1 +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 `
|
|
1
|
+
{"version":3,"file":"codecs.cjs","names":[],"sources":["../src/codecs.ts"],"sourcesContent":["import type { QueryParams, QueryParamsInput, RemoteSourceQuery, SourceQuery } from './types';\n\nimport { devOnly, warn } from './_dev';\nimport { sanitizeForLog } from './_utils';\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 `source.patch(...)`.\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 // Deliberately not `_utils.ts::clampPositiveInt` here: that one truncates a fractional\n // config value (2.5 -> 2) as a harmless normalization, and always floors invalid input to 1.\n // A URL param is untrusted network input, not a caller's own config object — \"2.5\" or \"-5\"\n // in a query string is more likely a corrupted/tampered link than a rounding artifact, so\n // this stays strict (reject anything that isn't a clean positive integer) and falls back to\n // the caller's own `defaultLimit`/`1`, not an unconditional floor of 1.\n //\n // @security `str` is the raw, attacker-controllable param value (this function's whole job\n // is parsing untrusted `location.search`/route params) — sanitize before interpolating into\n // the warning to prevent terminal-escape-sequence injection into a developer's console.\n const parsePositiveInt = (fieldName: string, 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 if (Number.isInteger(n) && n > 0) return n;\n\n devOnly(() =>\n warn(`decodeQuery: ${fieldName} \"${sanitizeForLog(str, 80)}\" is not a positive integer — using ${fallback}.`),\n );\n\n return fallback;\n };\n\n // @security Same as `parsePositiveInt` above — `str` is untrusted and may end up in a\n // terminal/log aggregator via this error's `.message`, not just a direct `console.warn`.\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}\": ${sanitizeForLog(str, 80)}`);\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('limit', rawLimit, defaultLimit),\n page: parsePositiveInt('page', rawPage, 1),\n };\n\n return result;\n};\n"],"mappings":"kFAmBA,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,EAYxC,GAAoB,EAAmB,EAAsC,IAA6B,CAC9G,GAAI,IAAU,IAAA,GAAW,OAAO,EAEhC,IAAM,EAAM,MAAM,QAAQ,CAAK,EAAI,EAAM,GAAK,EACxC,EAAI,OAAO,CAAG,EAQpB,OANI,OAAO,UAAU,CAAC,GAAK,EAAI,EAAU,GAEzC,EAAA,YACE,EAAA,KAAK,gBAAgB,EAAU,IAAI,EAAA,eAAe,EAAK,EAAE,EAAE,sCAAsC,EAAS,EAAE,CAC9G,EAEO,EACT,EAIM,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,EAAA,eAAe,EAAK,EAAE,GAAG,EAE/F,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,QAAS,EAAU,CAAY,EACvD,KAAM,EAAiB,OAAQ,EAAS,CAAC,CAGpC,CACT"}
|
package/dist/codecs.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type DecodeQueryOptions = Readonly<{
|
|
|
22
22
|
* Accepts either a plain `Record<string, string | string[] | undefined>` or a `URLSearchParams` instance.
|
|
23
23
|
*
|
|
24
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 `
|
|
25
|
+
* with a runtime schema (e.g. Zod) before passing to `source.patch(...)`.
|
|
26
26
|
* - `search` is omitted from the result when the param is absent (rather than defaulting to `''`).
|
|
27
27
|
* - `limit` and `page` are parsed as positive integers; invalid values fall back to defaults.
|
|
28
28
|
*/
|
package/dist/codecs.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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;AAM7F;;;;;;;;;;;;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,CAiE7C,CAAC"}
|
package/dist/codecs.js
CHANGED
|
@@ -1,36 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { devOnly as e, warn as t } from "./_dev.js";
|
|
2
|
+
import { sanitizeForLog as n } from "./_utils.js";
|
|
3
|
+
import { SourcererError as r } from "./errors.js";
|
|
2
4
|
//#region src/codecs.ts
|
|
3
|
-
var
|
|
5
|
+
var i = (e) => {
|
|
4
6
|
let t = {
|
|
5
7
|
...e.search && { search: e.search },
|
|
6
8
|
limit: String(e.limit),
|
|
7
9
|
page: String(e.page)
|
|
8
10
|
}, n = e;
|
|
9
11
|
return n.filter !== void 0 && (t.filter = JSON.stringify(n.filter)), n.sort !== void 0 && (t.sort = JSON.stringify(n.sort)), t;
|
|
10
|
-
},
|
|
11
|
-
let
|
|
12
|
-
if (
|
|
13
|
-
let
|
|
14
|
-
return Number.isInteger(
|
|
15
|
-
},
|
|
16
|
-
if (
|
|
17
|
-
let
|
|
12
|
+
}, a = (i, a = {}) => {
|
|
13
|
+
let o = i instanceof URLSearchParams ? Object.fromEntries(i.entries()) : i, { defaultLimit: s = 20, strict: c = !1 } = a, l = (r, i, a) => {
|
|
14
|
+
if (i === void 0) return a;
|
|
15
|
+
let o = Array.isArray(i) ? i[0] : i, s = Number(o);
|
|
16
|
+
return Number.isInteger(s) && s > 0 ? s : (e(() => t(`decodeQuery: ${r} "${n(o, 80)}" is not a positive integer — using ${a}.`)), a);
|
|
17
|
+
}, u = (e, t) => {
|
|
18
|
+
if (t === void 0) return;
|
|
19
|
+
let i = Array.isArray(t) ? t[0] : t;
|
|
18
20
|
try {
|
|
19
|
-
return JSON.parse(
|
|
21
|
+
return JSON.parse(i);
|
|
20
22
|
} catch {
|
|
21
|
-
if (
|
|
23
|
+
if (c) throw new r(`Invalid query param "${e}": ${n(i, 80)}`);
|
|
22
24
|
return;
|
|
23
25
|
}
|
|
24
|
-
},
|
|
26
|
+
}, d = o.limit, f = o.page, p = o.search, m = o.filter, h = o.sort, g = m === void 0 ? void 0 : u("filter", m), _ = h === void 0 ? void 0 : u("sort", h);
|
|
25
27
|
return {
|
|
26
|
-
...
|
|
27
|
-
...
|
|
28
|
-
...
|
|
29
|
-
limit:
|
|
30
|
-
page:
|
|
28
|
+
...g !== void 0 && { filter: g },
|
|
29
|
+
..._ !== void 0 && { sort: _ },
|
|
30
|
+
...p !== void 0 && { search: Array.isArray(p) ? p[0] : p },
|
|
31
|
+
limit: l("limit", d, s),
|
|
32
|
+
page: l("page", f, 1)
|
|
31
33
|
};
|
|
32
34
|
};
|
|
33
35
|
//#endregion
|
|
34
|
-
export {
|
|
36
|
+
export { a as decodeQuery, i as encodeQuery };
|
|
35
37
|
|
|
36
38
|
//# sourceMappingURL=codecs.js.map
|
package/dist/codecs.js.map
CHANGED
|
@@ -1 +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 `
|
|
1
|
+
{"version":3,"file":"codecs.js","names":[],"sources":["../src/codecs.ts"],"sourcesContent":["import type { QueryParams, QueryParamsInput, RemoteSourceQuery, SourceQuery } from './types';\n\nimport { devOnly, warn } from './_dev';\nimport { sanitizeForLog } from './_utils';\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 `source.patch(...)`.\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 // Deliberately not `_utils.ts::clampPositiveInt` here: that one truncates a fractional\n // config value (2.5 -> 2) as a harmless normalization, and always floors invalid input to 1.\n // A URL param is untrusted network input, not a caller's own config object — \"2.5\" or \"-5\"\n // in a query string is more likely a corrupted/tampered link than a rounding artifact, so\n // this stays strict (reject anything that isn't a clean positive integer) and falls back to\n // the caller's own `defaultLimit`/`1`, not an unconditional floor of 1.\n //\n // @security `str` is the raw, attacker-controllable param value (this function's whole job\n // is parsing untrusted `location.search`/route params) — sanitize before interpolating into\n // the warning to prevent terminal-escape-sequence injection into a developer's console.\n const parsePositiveInt = (fieldName: string, 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 if (Number.isInteger(n) && n > 0) return n;\n\n devOnly(() =>\n warn(`decodeQuery: ${fieldName} \"${sanitizeForLog(str, 80)}\" is not a positive integer — using ${fallback}.`),\n );\n\n return fallback;\n };\n\n // @security Same as `parsePositiveInt` above — `str` is untrusted and may end up in a\n // terminal/log aggregator via this error's `.message`, not just a direct `console.warn`.\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}\": ${sanitizeForLog(str, 80)}`);\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('limit', rawLimit, defaultLimit),\n page: parsePositiveInt('page', rawPage, 1),\n };\n\n return result;\n};\n"],"mappings":";;;;AAmBA,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,GAYxC,KAAoB,GAAmB,GAAsC,MAA6B;EAC9G,IAAI,MAAU,KAAA,GAAW,OAAO;EAEhC,IAAM,IAAM,MAAM,QAAQ,CAAK,IAAI,EAAM,KAAK,GACxC,IAAI,OAAO,CAAG;EAQpB,OANI,OAAO,UAAU,CAAC,KAAK,IAAI,IAAU,KAEzC,QACE,EAAK,gBAAgB,EAAU,IAAI,EAAe,GAAK,EAAE,EAAE,sCAAsC,EAAS,EAAE,CAC9G,GAEO;CACT,GAIM,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,EAAe,GAAK,EAAE,GAAG;GAE/F;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,SAAS,GAAU,CAAY;EACvD,MAAM,EAAiB,QAAQ,GAAS,CAAC;CAGpC;AACT"}
|
package/dist/core.cjs
CHANGED
|
@@ -1,2 +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.
|
|
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.SourcererDisposedError);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.SourcererDisposedError):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.SourcererTimeoutError(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
2
|
//# sourceMappingURL=core.cjs.map
|
package/dist/core.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.cjs","names":[],"sources":["../src/core.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"core.cjs","names":[],"sources":["../src/core.ts"],"sourcesContent":["import { SourcererDisposedError, SourcererTimeoutError } from './errors';\n\n/**\n * Shared source infrastructure: change notification, debounce scheduling, and ready() polling.\n * All source factories compose this core rather than duplicating the same patterns.\n *\n * Change notification is a plain `Set<listener>` — deliberately NOT `@vielzeug/ripple`, despite\n * that being the repo-wide convention for \"reactive\" packages. Tried it; reverted. Two concrete\n * problems, not just aesthetics:\n *\n * 1. `prefetchSource()`/`prefetchSourceAndKeep()` are explicitly documented for SSR, and every\n * source factory routes through this file's `signal`-backed notify. Ripple's own scheduler\n * warns that its module-level flush queue is shared across concurrent requests in Node.js\n * unless the caller opts into `@vielzeug/ripple/ssr`'s per-request isolation — a real,\n * unaddressed risk in exactly the code path meant to run on a server handling concurrent\n * requests.\n * 2. `subscribe()`'s public contract is \"fire on every notify()\", not per-field reactivity —\n * ripple's dependency-tracking machinery (a real subscription object per `watch()` call)\n * buys nothing over a bare `Set.add()` for that contract, since there's exactly one thing\n * ever being watched (an internal version counter, not real data).\n *\n * `current`/`meta` themselves were never ripple signals either way — the public shape has\n * always been plain getters + `subscribe()`, framework-agnostic by design (see `types.ts`'s\n * `ReactiveSource` doc comment). Nothing about restoring a plain `Set` changes that contract.\n *\n * Debounce scheduling and `ready()`'s \"resolve when idle, with a timeout\" polling were never\n * ripple's job either way: ripple is a dependency-tracking signal system (\"notify when a value\n * changes\"), not an async-coordination library (\"wait for a condition, with a timeout, and\n * reject if disposed first\").\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 SourcererDisposedError());\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 SourcererDisposedError());\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 SourcererTimeoutError(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":"gCA+EA,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,sBAAwB,EAG5C,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,sBAAwB,EAE5D,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,sBAAsB,CAAS,CAAC,CAC7C,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
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared source infrastructure:
|
|
2
|
+
* Shared source infrastructure: change notification, debounce scheduling, and ready() polling.
|
|
3
3
|
* All source factories compose this core rather than duplicating the same patterns.
|
|
4
|
+
*
|
|
5
|
+
* Change notification is a plain `Set<listener>` — deliberately NOT `@vielzeug/ripple`, despite
|
|
6
|
+
* that being the repo-wide convention for "reactive" packages. Tried it; reverted. Two concrete
|
|
7
|
+
* problems, not just aesthetics:
|
|
8
|
+
*
|
|
9
|
+
* 1. `prefetchSource()`/`prefetchSourceAndKeep()` are explicitly documented for SSR, and every
|
|
10
|
+
* source factory routes through this file's `signal`-backed notify. Ripple's own scheduler
|
|
11
|
+
* warns that its module-level flush queue is shared across concurrent requests in Node.js
|
|
12
|
+
* unless the caller opts into `@vielzeug/ripple/ssr`'s per-request isolation — a real,
|
|
13
|
+
* unaddressed risk in exactly the code path meant to run on a server handling concurrent
|
|
14
|
+
* requests.
|
|
15
|
+
* 2. `subscribe()`'s public contract is "fire on every notify()", not per-field reactivity —
|
|
16
|
+
* ripple's dependency-tracking machinery (a real subscription object per `watch()` call)
|
|
17
|
+
* buys nothing over a bare `Set.add()` for that contract, since there's exactly one thing
|
|
18
|
+
* ever being watched (an internal version counter, not real data).
|
|
19
|
+
*
|
|
20
|
+
* `current`/`meta` themselves were never ripple signals either way — the public shape has
|
|
21
|
+
* always been plain getters + `subscribe()`, framework-agnostic by design (see `types.ts`'s
|
|
22
|
+
* `ReactiveSource` doc comment). Nothing about restoring a plain `Set` changes that contract.
|
|
23
|
+
*
|
|
24
|
+
* Debounce scheduling and `ready()`'s "resolve when idle, with a timeout" polling were never
|
|
25
|
+
* ripple's job either way: ripple is a dependency-tracking signal system ("notify when a value
|
|
26
|
+
* changes"), not an async-coordination library ("wait for a condition, with a timeout, and
|
|
27
|
+
* reject if disposed first").
|
|
4
28
|
*/
|
|
5
29
|
export type SourceCore = {
|
|
6
30
|
/** Cancels any pending timer without invoking the callback. */
|
package/dist/core.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;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
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SourcererDisposedError as e, SourcererTimeoutError as t } from "./errors.js";
|
|
2
2
|
//#region src/core.ts
|
|
3
3
|
function n(n) {
|
|
4
4
|
let r = /* @__PURE__ */ new Set(), i = /* @__PURE__ */ new Set(), a = new AbortController(), o, s = !1;
|
package/dist/core.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.js","names":[],"sources":["../src/core.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"core.js","names":[],"sources":["../src/core.ts"],"sourcesContent":["import { SourcererDisposedError, SourcererTimeoutError } from './errors';\n\n/**\n * Shared source infrastructure: change notification, debounce scheduling, and ready() polling.\n * All source factories compose this core rather than duplicating the same patterns.\n *\n * Change notification is a plain `Set<listener>` — deliberately NOT `@vielzeug/ripple`, despite\n * that being the repo-wide convention for \"reactive\" packages. Tried it; reverted. Two concrete\n * problems, not just aesthetics:\n *\n * 1. `prefetchSource()`/`prefetchSourceAndKeep()` are explicitly documented for SSR, and every\n * source factory routes through this file's `signal`-backed notify. Ripple's own scheduler\n * warns that its module-level flush queue is shared across concurrent requests in Node.js\n * unless the caller opts into `@vielzeug/ripple/ssr`'s per-request isolation — a real,\n * unaddressed risk in exactly the code path meant to run on a server handling concurrent\n * requests.\n * 2. `subscribe()`'s public contract is \"fire on every notify()\", not per-field reactivity —\n * ripple's dependency-tracking machinery (a real subscription object per `watch()` call)\n * buys nothing over a bare `Set.add()` for that contract, since there's exactly one thing\n * ever being watched (an internal version counter, not real data).\n *\n * `current`/`meta` themselves were never ripple signals either way — the public shape has\n * always been plain getters + `subscribe()`, framework-agnostic by design (see `types.ts`'s\n * `ReactiveSource` doc comment). Nothing about restoring a plain `Set` changes that contract.\n *\n * Debounce scheduling and `ready()`'s \"resolve when idle, with a timeout\" polling were never\n * ripple's job either way: ripple is a dependency-tracking signal system (\"notify when a value\n * changes\"), not an async-coordination library (\"wait for a condition, with a timeout, and\n * reject if disposed first\").\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 SourcererDisposedError());\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 SourcererDisposedError());\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 SourcererTimeoutError(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":";;AA+EA,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,EAAuB,CAAC;GAM5C,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,EAAuB,CAAC,IAE5D,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,EAAsB,CAAS,CAAC;IAC7C,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"}
|
package/dist/cursorSource.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./
|
|
1
|
+
const e=require("./_utils.cjs"),t=require("./errors.cjs"),n=require("./asyncSearch.cjs"),r=require("./asyncSource.cjs");let i=require("@vielzeug/arsenal");function a(a){let o=a.queryKey??e.defaultKeyOf,s=e.clampPositiveInt(a.limit??20,`createCursorSource`,`limit`),c=``,l,u,d,f,p=[],m=0,h=null,g=[],_={error:null,hasNextPage:!1,hasPrevPage:!1,isLoading:!1,isSearchPending:!1,pageSize:s,totalItems:0},v=()=>{g=p,_={error:h,hasNextPage:d!==void 0,hasPrevPage:f!==void 0,isLoading:y.pendingCount()>0,isSearchPending:y.core.isScheduled,pageSize:s,totalItems:m}},y=r.createAsyncSource(a,o,v),{autoFetch:b,debounceMs:x,retryAttempts:S,retryDelay:C}=y;v();let w=()=>{y.disposed||y.core.notify()},T=n.createAsyncSearchCoordinator(y.core,w),E=()=>({...l!==void 0&&{after:l},...u!==void 0&&{before:u},...c&&{search:c},limit:s}),D=n=>(h=null,y.fetch(n,async(n,r,o)=>{let s=Date.now();try{let e=await(0,i.retry)(e=>a.fetch(n,e),{delay:C,signal:r,times:S+1});o()&&(p=e.items,m=e.total??e.items.length,d=e.nextCursor,f=e.prevCursor,h=null,a.onFetch?.({durationMs:Date.now()-s,query:n,status:`success`}))}catch(i){if(r.aborted)return;o()&&(p=[],m=0,d=void 0,f=void 0,h=new t.SourcererError(e.extractError(i),{cause:i,context:{kind:`cursor`,limit:n.limit,...n.search&&{search:n.search}}}),a.onFetch?.({durationMs:Date.now()-s,error:h,query:n,status:`error`}))}},()=>{w(),T.settleIfIdle(y.pendingCount())})),O=()=>D(E());return b&&O(),y.startRefreshInterval(()=>void O()),{get current(){return g},get disposalSignal(){return y.disposalSignal},dispose:()=>{T.cancel(),T.dispose(),y.dispose()},get disposed(){return y.disposed},get meta(){return _},next(){return d?(l=d,u=void 0,O()):Promise.resolve()},patch(t){let n=!1;if(t.limit!==void 0){let r=e.clampPositiveInt(t.limit,`source.patch`,`limit`);r!==s&&(s=r,l=void 0,u=void 0,n=!0)}return`search`in t&&(t.search!==c||y.core.isScheduled)&&(c=t.search??``,l=void 0,u=void 0,n=!0),n?(T.cancel(),O()):Promise.resolve()},prev(){return f?(u=f,l=void 0,O()):Promise.resolve()},get query(){return{...l!==void 0&&{after:l},...u!==void 0&&{before:u},...c&&{search:c},limit:s}},ready(e){return y.ready(e)},refresh(){return O()},reset(){return T.cancel(),c=``,l=void 0,u=void 0,O()},search(e,t){return t?.immediate?e===c&&!y.core.isScheduled?Promise.resolve():(T.cancel(),c=e,l=void 0,u=void 0,O()):e===c?Promise.resolve():(c=e,l=void 0,u=void 0,T.schedule(()=>void O(),x))},subscribe:e=>y.core.subscribe(e),[Symbol.dispose]:()=>{T.cancel(),T.dispose(),y.dispose()}}}exports.createCursorSource=a;
|
|
2
2
|
//# sourceMappingURL=cursorSource.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cursorSource.cjs","names":[],"sources":["../src/cursorSource.ts"],"sourcesContent":["import type { CursorConfig, CursorMeta, CursorSource, CursorSourceQuery, SearchOptions } from './types';\n\nimport { defaultKeyOf, extractError, retry } from './_utils';\nimport { createAsyncSearchCoordinator } from './asyncSearch';\nimport { createAsyncSource } from './asyncSource';\nimport { SourcererError } from './errors';\n\n/** Creates a cursor-based (keyset-pagination) source that fetches data from a network endpoint. */\nexport function createCursorSource<T, TCursor = string>(cfg: CursorConfig<T, TCursor>): CursorSource<T, TCursor> {\n const keyOf = cfg.queryKey ?? defaultKeyOf;\n\n // ── Mutable state ───────────────────────────────────────────────────────────\n let limit = Math.max(1, Math.trunc(cfg.limit ?? 20));\n let search = '';\n let afterCursor: TCursor | undefined;\n let beforeCursor: TCursor | undefined;\n let nextCursor: TCursor | undefined;\n let prevCursor: TCursor | undefined;\n let items: readonly T[] = [];\n let total = 0;\n let error: SourcererError | null = null;\n\n // ── Cached accessors ────────────────────────────────────────────────────────\n let cachedCurrent: readonly T[] = [];\n let cachedMeta: CursorMeta = {\n error: null,\n hasNextPage: false,\n hasPrevPage: false,\n isLoading: false,\n isSearchPending: false,\n pageSize: limit,\n totalItems: 0,\n };\n\n const refreshMeta = () => {\n cachedCurrent = items;\n cachedMeta = {\n error,\n hasNextPage: nextCursor !== undefined,\n hasPrevPage: prevCursor !== undefined,\n isLoading: base.pendingCount() > 0,\n isSearchPending: base.core.isScheduled,\n pageSize: limit,\n totalItems: total,\n };\n };\n\n // onBeforeNotify ensures refreshMeta() runs before any subscriber observes the new state,\n // eliminating the need for a parallel listeners Set.\n const base = createAsyncSource<CursorSourceQuery<TCursor>>(cfg, keyOf, refreshMeta);\n const { autoFetch, debounceMs, retryAttempts, retryDelay } = base;\n\n refreshMeta();\n\n const notifyListeners = () => {\n if (base.disposed) return;\n\n base.core.notify();\n };\n const searchCoordinator = createAsyncSearchCoordinator(base.core, notifyListeners);\n\n // ── Core fetch ──────────────────────────────────────────────────────────────\n const toRemoteQuery = (): CursorSourceQuery<TCursor> => ({\n ...(afterCursor !== undefined && { after: afterCursor }),\n ...(beforeCursor !== undefined && { before: beforeCursor }),\n ...(search && { search }),\n limit,\n });\n\n const fetchQuery = (q: CursorSourceQuery<TCursor>): Promise<void> => {\n error = null;\n\n return base.fetch(\n q,\n async (q, signal, isLatest) => {\n const startTime = Date.now();\n\n try {\n const result = await retry((sig) => cfg.fetch(q, sig!), {\n delay: retryDelay,\n signal,\n times: retryAttempts + 1,\n });\n\n if (isLatest()) {\n items = result.items;\n total = result.total ?? result.items.length;\n nextCursor = result.nextCursor;\n prevCursor = result.prevCursor;\n error = null;\n cfg.onFetch?.({ durationMs: Date.now() - startTime, query: q, status: 'success' });\n }\n } catch (reason: unknown) {\n if (signal.aborted) return;\n\n if (isLatest()) {\n items = [];\n total = 0;\n nextCursor = undefined;\n prevCursor = undefined;\n error = new SourcererError(extractError(reason), {\n cause: reason,\n context: { kind: 'cursor', limit: q.limit, ...(q.search && { search: q.search }) },\n });\n cfg.onFetch?.({ durationMs: Date.now() - startTime, error, query: q, status: 'error' });\n }\n }\n },\n () => {\n notifyListeners();\n\n searchCoordinator.settleIfIdle(base.pendingCount());\n },\n );\n };\n\n const doUpdate = () => fetchQuery(toRemoteQuery());\n\n // ── Auto-fetch + refresh interval ───────────────────────────────────────────\n if (autoFetch) void doUpdate();\n\n base.startRefreshInterval(() => void doUpdate());\n\n // ── Public API ──────────────────────────────────────────────────────────────\n return {\n get current() {\n return cachedCurrent;\n },\n\n get disposalSignal() {\n return base.disposalSignal;\n },\n\n dispose: () => {\n searchCoordinator.cancel();\n searchCoordinator.dispose();\n base.dispose();\n },\n\n get disposed() {\n return base.disposed;\n },\n\n get meta() {\n return cachedMeta;\n },\n\n next() {\n if (!nextCursor) return Promise.resolve();\n\n afterCursor = nextCursor;\n beforeCursor = undefined;\n\n return doUpdate();\n },\n\n patch(changes) {\n let changed = false;\n\n if (changes.limit !== undefined) {\n const next = Math.max(1, Math.trunc(changes.limit));\n\n if (next !== limit) {\n limit = next;\n afterCursor = undefined;\n beforeCursor = undefined;\n changed = true;\n }\n }\n\n // `|| base.core.isScheduled` also catches patching in the same search text as an already-pending\n // debounced search() call — that still needs flushing, not silently dropping as a no-op.\n if ('search' in changes && (changes.search !== search || base.core.isScheduled)) {\n search = changes.search ?? '';\n afterCursor = undefined;\n beforeCursor = undefined;\n changed = true;\n }\n\n if (!changed) return Promise.resolve();\n\n // patch() is documented as a single atomic fetch — cancel any debounced search()\n // still pending so it doesn't fire a second, redundant fetch afterwards.\n searchCoordinator.cancel();\n\n return doUpdate();\n },\n\n prev() {\n if (!prevCursor) return Promise.resolve();\n\n beforeCursor = prevCursor;\n afterCursor = undefined;\n\n return doUpdate();\n },\n\n get query() {\n return {\n ...(afterCursor !== undefined && { after: afterCursor }),\n ...(beforeCursor !== undefined && { before: beforeCursor }),\n ...(search && { search }),\n limit,\n };\n },\n\n ready(timeout) {\n return base.ready(timeout);\n },\n\n refresh() {\n return doUpdate();\n },\n\n reset() {\n searchCoordinator.cancel();\n search = '';\n afterCursor = undefined;\n beforeCursor = undefined;\n\n return doUpdate();\n },\n\n search(q, opts?: SearchOptions): Promise<void> {\n if (opts?.immediate) {\n // A pending debounced search for this same text still needs flushing —\n // `q === search` alone doesn't mean the fetch already happened.\n if (q === search && !base.core.isScheduled) return Promise.resolve();\n\n searchCoordinator.cancel();\n search = q;\n afterCursor = undefined;\n beforeCursor = undefined;\n\n return doUpdate();\n }\n\n if (q === search) return Promise.resolve();\n\n search = q;\n afterCursor = undefined;\n beforeCursor = undefined;\n\n return searchCoordinator.schedule(() => void doUpdate(), debounceMs);\n },\n\n subscribe: (listener) => base.core.subscribe(listener),\n\n [Symbol.dispose]: () => {\n searchCoordinator.cancel();\n searchCoordinator.dispose();\n base.dispose();\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cursorSource.cjs","names":[],"sources":["../src/cursorSource.ts"],"sourcesContent":["import type { CursorConfig, CursorMeta, CursorSource, CursorSourceQuery, SearchOptions } from './types';\n\nimport { clampPositiveInt, defaultKeyOf, extractError, retry } from './_utils';\nimport { createAsyncSearchCoordinator } from './asyncSearch';\nimport { createAsyncSource } from './asyncSource';\nimport { SourcererError } from './errors';\n\n/** Creates a cursor-based (keyset-pagination) source that fetches data from a network endpoint. */\nexport function createCursorSource<T, TCursor = string>(cfg: CursorConfig<T, TCursor>): CursorSource<T, TCursor> {\n const keyOf = cfg.queryKey ?? defaultKeyOf;\n\n // ── Mutable state ───────────────────────────────────────────────────────────\n let limit = clampPositiveInt(cfg.limit ?? 20, 'createCursorSource', 'limit');\n let search = '';\n let afterCursor: TCursor | undefined;\n let beforeCursor: TCursor | undefined;\n let nextCursor: TCursor | undefined;\n let prevCursor: TCursor | undefined;\n let items: readonly T[] = [];\n let total = 0;\n let error: SourcererError | null = null;\n\n // ── Cached accessors ────────────────────────────────────────────────────────\n let cachedCurrent: readonly T[] = [];\n let cachedMeta: CursorMeta = {\n error: null,\n hasNextPage: false,\n hasPrevPage: false,\n isLoading: false,\n isSearchPending: false,\n pageSize: limit,\n totalItems: 0,\n };\n\n const refreshMeta = () => {\n cachedCurrent = items;\n cachedMeta = {\n error,\n hasNextPage: nextCursor !== undefined,\n hasPrevPage: prevCursor !== undefined,\n isLoading: base.pendingCount() > 0,\n isSearchPending: base.core.isScheduled,\n pageSize: limit,\n totalItems: total,\n };\n };\n\n // onBeforeNotify ensures refreshMeta() runs before any subscriber observes the new state,\n // eliminating the need for a parallel listeners Set.\n const base = createAsyncSource<CursorSourceQuery<TCursor>>(cfg, keyOf, refreshMeta);\n const { autoFetch, debounceMs, retryAttempts, retryDelay } = base;\n\n refreshMeta();\n\n const notifyListeners = () => {\n if (base.disposed) return;\n\n base.core.notify();\n };\n const searchCoordinator = createAsyncSearchCoordinator(base.core, notifyListeners);\n\n // ── Core fetch ──────────────────────────────────────────────────────────────\n const toRemoteQuery = (): CursorSourceQuery<TCursor> => ({\n ...(afterCursor !== undefined && { after: afterCursor }),\n ...(beforeCursor !== undefined && { before: beforeCursor }),\n ...(search && { search }),\n limit,\n });\n\n const fetchQuery = (q: CursorSourceQuery<TCursor>): Promise<void> => {\n error = null;\n\n return base.fetch(\n q,\n async (q, signal, isLatest) => {\n const startTime = Date.now();\n\n try {\n const result = await retry((sig) => cfg.fetch(q, sig!), {\n delay: retryDelay,\n signal,\n times: retryAttempts + 1,\n });\n\n if (isLatest()) {\n items = result.items;\n total = result.total ?? result.items.length;\n nextCursor = result.nextCursor;\n prevCursor = result.prevCursor;\n error = null;\n cfg.onFetch?.({ durationMs: Date.now() - startTime, query: q, status: 'success' });\n }\n } catch (reason: unknown) {\n if (signal.aborted) return;\n\n if (isLatest()) {\n items = [];\n total = 0;\n nextCursor = undefined;\n prevCursor = undefined;\n error = new SourcererError(extractError(reason), {\n cause: reason,\n context: { kind: 'cursor', limit: q.limit, ...(q.search && { search: q.search }) },\n });\n cfg.onFetch?.({ durationMs: Date.now() - startTime, error, query: q, status: 'error' });\n }\n }\n },\n () => {\n notifyListeners();\n\n searchCoordinator.settleIfIdle(base.pendingCount());\n },\n );\n };\n\n const doUpdate = () => fetchQuery(toRemoteQuery());\n\n // ── Auto-fetch + refresh interval ───────────────────────────────────────────\n if (autoFetch) void doUpdate();\n\n base.startRefreshInterval(() => void doUpdate());\n\n // ── Public API ──────────────────────────────────────────────────────────────\n return {\n get current() {\n return cachedCurrent;\n },\n\n get disposalSignal() {\n return base.disposalSignal;\n },\n\n dispose: () => {\n searchCoordinator.cancel();\n searchCoordinator.dispose();\n base.dispose();\n },\n\n get disposed() {\n return base.disposed;\n },\n\n get meta() {\n return cachedMeta;\n },\n\n next() {\n if (!nextCursor) return Promise.resolve();\n\n afterCursor = nextCursor;\n beforeCursor = undefined;\n\n return doUpdate();\n },\n\n patch(changes) {\n let changed = false;\n\n if (changes.limit !== undefined) {\n const next = clampPositiveInt(changes.limit, 'source.patch', 'limit');\n\n if (next !== limit) {\n limit = next;\n afterCursor = undefined;\n beforeCursor = undefined;\n changed = true;\n }\n }\n\n // `|| base.core.isScheduled` also catches patching in the same search text as an already-pending\n // debounced search() call — that still needs flushing, not silently dropping as a no-op.\n if ('search' in changes && (changes.search !== search || base.core.isScheduled)) {\n search = changes.search ?? '';\n afterCursor = undefined;\n beforeCursor = undefined;\n changed = true;\n }\n\n if (!changed) return Promise.resolve();\n\n // patch() is documented as a single atomic fetch — cancel any debounced search()\n // still pending so it doesn't fire a second, redundant fetch afterwards.\n searchCoordinator.cancel();\n\n return doUpdate();\n },\n\n prev() {\n if (!prevCursor) return Promise.resolve();\n\n beforeCursor = prevCursor;\n afterCursor = undefined;\n\n return doUpdate();\n },\n\n get query() {\n return {\n ...(afterCursor !== undefined && { after: afterCursor }),\n ...(beforeCursor !== undefined && { before: beforeCursor }),\n ...(search && { search }),\n limit,\n };\n },\n\n ready(timeout) {\n return base.ready(timeout);\n },\n\n refresh() {\n return doUpdate();\n },\n\n reset() {\n searchCoordinator.cancel();\n search = '';\n afterCursor = undefined;\n beforeCursor = undefined;\n\n return doUpdate();\n },\n\n search(q, opts?: SearchOptions): Promise<void> {\n if (opts?.immediate) {\n // A pending debounced search for this same text still needs flushing —\n // `q === search` alone doesn't mean the fetch already happened.\n if (q === search && !base.core.isScheduled) return Promise.resolve();\n\n searchCoordinator.cancel();\n search = q;\n afterCursor = undefined;\n beforeCursor = undefined;\n\n return doUpdate();\n }\n\n if (q === search) return Promise.resolve();\n\n search = q;\n afterCursor = undefined;\n beforeCursor = undefined;\n\n return searchCoordinator.schedule(() => void doUpdate(), debounceMs);\n },\n\n subscribe: (listener) => base.core.subscribe(listener),\n\n [Symbol.dispose]: () => {\n searchCoordinator.cancel();\n searchCoordinator.dispose();\n base.dispose();\n },\n };\n}\n"],"mappings":"2JAQA,SAAgB,EAAwC,EAAyD,CAC/G,IAAM,EAAQ,EAAI,UAAY,EAAA,aAG1B,EAAQ,EAAA,iBAAiB,EAAI,OAAS,GAAI,qBAAsB,OAAO,EACvE,EAAS,GACT,EACA,EACA,EACA,EACA,EAAsB,CAAC,EACvB,EAAQ,EACR,EAA+B,KAG/B,EAA8B,CAAC,EAC/B,EAAyB,CAC3B,MAAO,KACP,YAAa,GACb,YAAa,GACb,UAAW,GACX,gBAAiB,GACjB,SAAU,EACV,WAAY,CACd,EAEM,MAAoB,CACxB,EAAgB,EAChB,EAAa,CACX,QACA,YAAa,IAAe,IAAA,GAC5B,YAAa,IAAe,IAAA,GAC5B,UAAW,EAAK,aAAa,EAAI,EACjC,gBAAiB,EAAK,KAAK,YAC3B,SAAU,EACV,WAAY,CACd,CACF,EAIM,EAAO,EAAA,kBAA8C,EAAK,EAAO,CAAW,EAC5E,CAAE,YAAW,aAAY,gBAAe,cAAe,EAE7D,EAAY,EAEZ,IAAM,MAAwB,CACxB,EAAK,UAET,EAAK,KAAK,OAAO,CACnB,EACM,EAAoB,EAAA,6BAA6B,EAAK,KAAM,CAAe,EAG3E,OAAmD,CACvD,GAAI,IAAgB,IAAA,IAAa,CAAE,MAAO,CAAY,EACtD,GAAI,IAAiB,IAAA,IAAa,CAAE,OAAQ,CAAa,EACzD,GAAI,GAAU,CAAE,QAAO,EACvB,OACF,GAEM,EAAc,IAClB,EAAQ,KAED,EAAK,MACV,EACA,MAAO,EAAG,EAAQ,IAAa,CAC7B,IAAM,EAAY,KAAK,IAAI,EAE3B,GAAI,CACF,IAAM,EAAS,MAAA,EAAA,EAAA,MAAA,CAAa,GAAQ,EAAI,MAAM,EAAG,CAAI,EAAG,CACtD,MAAO,EACP,SACA,MAAO,EAAgB,CACzB,CAAC,EAEG,EAAS,IACX,EAAQ,EAAO,MACf,EAAQ,EAAO,OAAS,EAAO,MAAM,OACrC,EAAa,EAAO,WACpB,EAAa,EAAO,WACpB,EAAQ,KACR,EAAI,UAAU,CAAE,WAAY,KAAK,IAAI,EAAI,EAAW,MAAO,EAAG,OAAQ,SAAU,CAAC,EAErF,OAAS,EAAiB,CACxB,GAAI,EAAO,QAAS,OAEhB,EAAS,IACX,EAAQ,CAAC,EACT,EAAQ,EACR,EAAa,IAAA,GACb,EAAa,IAAA,GACb,EAAQ,IAAI,EAAA,eAAe,EAAA,aAAa,CAAM,EAAG,CAC/C,MAAO,EACP,QAAS,CAAE,KAAM,SAAU,MAAO,EAAE,MAAO,GAAI,EAAE,QAAU,CAAE,OAAQ,EAAE,MAAO,CAAG,CACnF,CAAC,EACD,EAAI,UAAU,CAAE,WAAY,KAAK,IAAI,EAAI,EAAW,QAAO,MAAO,EAAG,OAAQ,OAAQ,CAAC,EAE1F,CACF,MACM,CACJ,EAAgB,EAEhB,EAAkB,aAAa,EAAK,aAAa,CAAC,CACpD,CACF,GAGI,MAAiB,EAAW,EAAc,CAAC,EAQjD,OALI,GAAW,EAAc,EAE7B,EAAK,yBAA2B,KAAK,EAAS,CAAC,EAGxC,CACL,IAAI,SAAU,CACZ,OAAO,CACT,EAEA,IAAI,gBAAiB,CACnB,OAAO,EAAK,cACd,EAEA,YAAe,CACb,EAAkB,OAAO,EACzB,EAAkB,QAAQ,EAC1B,EAAK,QAAQ,CACf,EAEA,IAAI,UAAW,CACb,OAAO,EAAK,QACd,EAEA,IAAI,MAAO,CACT,OAAO,CACT,EAEA,MAAO,CAML,OALK,GAEL,EAAc,EACd,EAAe,IAAA,GAER,EAAS,GALQ,QAAQ,QAAQ,CAM1C,EAEA,MAAM,EAAS,CACb,IAAI,EAAU,GAEd,GAAI,EAAQ,QAAU,IAAA,GAAW,CAC/B,IAAM,EAAO,EAAA,iBAAiB,EAAQ,MAAO,eAAgB,OAAO,EAEhE,IAAS,IACX,EAAQ,EACR,EAAc,IAAA,GACd,EAAe,IAAA,GACf,EAAU,GAEd,CAiBA,MAbI,WAAY,IAAY,EAAQ,SAAW,GAAU,EAAK,KAAK,eACjE,EAAS,EAAQ,QAAU,GAC3B,EAAc,IAAA,GACd,EAAe,IAAA,GACf,EAAU,IAGP,GAIL,EAAkB,OAAO,EAElB,EAAS,GANK,QAAQ,QAAQ,CAOvC,EAEA,MAAO,CAML,OALK,GAEL,EAAe,EACf,EAAc,IAAA,GAEP,EAAS,GALQ,QAAQ,QAAQ,CAM1C,EAEA,IAAI,OAAQ,CACV,MAAO,CACL,GAAI,IAAgB,IAAA,IAAa,CAAE,MAAO,CAAY,EACtD,GAAI,IAAiB,IAAA,IAAa,CAAE,OAAQ,CAAa,EACzD,GAAI,GAAU,CAAE,QAAO,EACvB,OACF,CACF,EAEA,MAAM,EAAS,CACb,OAAO,EAAK,MAAM,CAAO,CAC3B,EAEA,SAAU,CACR,OAAO,EAAS,CAClB,EAEA,OAAQ,CAMN,OALA,EAAkB,OAAO,EACzB,EAAS,GACT,EAAc,IAAA,GACd,EAAe,IAAA,GAER,EAAS,CAClB,EAEA,OAAO,EAAG,EAAqC,CAoB7C,OAnBI,GAAM,UAGJ,IAAM,GAAU,CAAC,EAAK,KAAK,YAAoB,QAAQ,QAAQ,GAEnE,EAAkB,OAAO,EACzB,EAAS,EACT,EAAc,IAAA,GACd,EAAe,IAAA,GAER,EAAS,GAGd,IAAM,EAAe,QAAQ,QAAQ,GAEzC,EAAS,EACT,EAAc,IAAA,GACd,EAAe,IAAA,GAER,EAAkB,aAAe,KAAK,EAAS,EAAG,CAAU,EACrE,EAEA,UAAY,GAAa,EAAK,KAAK,UAAU,CAAQ,GAEpD,OAAO,aAAgB,CACtB,EAAkB,OAAO,EACzB,EAAkB,QAAQ,EAC1B,EAAK,QAAQ,CACf,CACF,CACF"}
|