@urbicon-ui/sveltekit-utils 6.22.0 → 6.24.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/cron.d.ts CHANGED
@@ -1,18 +1,96 @@
1
+ /** One scheduled job: an endpoint to hit and how often. */
1
2
  export interface CronJob {
3
+ /** Path (appended to {@link CronRunnerConfig.baseUrl}) to fetch on each tick. */
2
4
  path: string;
5
+ /**
6
+ * Interval between fires, in seconds. The first fire happens *after* one
7
+ * interval — there is no leading call at `start()`.
8
+ */
3
9
  intervalSeconds: number;
10
+ /**
11
+ * HTTP method for the request.
12
+ * @default 'POST'
13
+ */
4
14
  method?: 'GET' | 'POST';
5
15
  }
16
+ /** Configuration for {@link createCronRunner}. */
6
17
  export interface CronRunnerConfig {
18
+ /**
19
+ * Shared secret sent on every request in the {@link secretHeader} header, so
20
+ * the target endpoint can distinguish a scheduled call from a public one.
21
+ * Keep it in a private env var; the endpoint compares against the same value.
22
+ */
7
23
  secret: string;
24
+ /**
25
+ * Header name carrying the {@link secret}.
26
+ * @default 'x-cron-secret'
27
+ */
8
28
  secretHeader?: string;
29
+ /**
30
+ * Origin the job paths are resolved against (e.g. `https://app.example.com`).
31
+ * @default 'http://localhost:3000'
32
+ */
9
33
  baseUrl?: string;
34
+ /** Jobs to schedule; each runs on its own independent interval. */
10
35
  jobs: CronJob[];
36
+ /**
37
+ * Called when a job fails, with the failing {@link CronJob} and an `Error`.
38
+ * Fires in two cases:
39
+ * - the job's `fetch` **rejects** (network error, DNS failure, abort) — the
40
+ * `Error` is whatever `fetch` threw.
41
+ * - the endpoint answers with a **non-2xx** status — the runner synthesises
42
+ * an `Error` naming the job and status, with the numeric code attached as
43
+ * `error.status` (e.g. `500`, `403`).
44
+ *
45
+ * Without a handler both failure modes are swallowed silently — pass one to
46
+ * observe per-run outcomes.
47
+ */
11
48
  onError?: (job: CronJob, error: Error) => void;
12
49
  }
50
+ /** Handle returned by {@link createCronRunner}. */
13
51
  export interface CronRunner {
52
+ /**
53
+ * Arm every job's interval timer. Idempotent — calling `start()` while
54
+ * already running is a no-op (does not double-schedule).
55
+ */
14
56
  start(): void;
57
+ /**
58
+ * Clear every timer so nothing fires again, and flip {@link isRunning} to
59
+ * `false`. Call this on shutdown / HMR teardown to avoid leaked intervals.
60
+ */
15
61
  stop(): void;
62
+ /** Whether the runner is currently armed (between `start()` and `stop()`). */
16
63
  isRunning(): boolean;
17
64
  }
65
+ /**
66
+ * Create a background runner that fires HTTP requests at SvelteKit server
67
+ * endpoints on a fixed interval — a minimal in-process cron for scheduled work
68
+ * (digests, cleanup, cache warming).
69
+ *
70
+ * Deliberately simple: one `setInterval` per job, no drift compensation, no
71
+ * distributed locking, no retry/backoff. Fits a **single-process** deployment;
72
+ * for scale-out point a real scheduler (BullMQ, a platform cron) at the same
73
+ * endpoints instead. The runner starts idle — call `start()` explicitly.
74
+ *
75
+ * @param config - Secret/header, base URL, and the jobs to schedule.
76
+ * @returns A {@link CronRunner} handle (`start` / `stop` / `isRunning`).
77
+ * @example
78
+ * ```typescript
79
+ * // src/lib/server/cron.ts
80
+ * import { createCronRunner } from '@urbicon-ui/sveltekit-utils/cron';
81
+ * import { env } from '$env/dynamic/private';
82
+ *
83
+ * export const cron = createCronRunner({
84
+ * secret: env.CRON_SECRET,
85
+ * baseUrl: env.BASE_URL,
86
+ * jobs: [
87
+ * { path: '/api/cron/send-digest', intervalSeconds: 3600 },
88
+ * { path: '/api/cron/cleanup', intervalSeconds: 900 }
89
+ * ],
90
+ * onError: (job, err) => console.error(`Cron ${job.path} failed`, err)
91
+ * });
92
+ *
93
+ * cron.start();
94
+ * ```
95
+ */
18
96
  export declare function createCronRunner(config: CronRunnerConfig): CronRunner;
package/dist/cron.js CHANGED
@@ -1,3 +1,34 @@
1
+ /**
2
+ * Create a background runner that fires HTTP requests at SvelteKit server
3
+ * endpoints on a fixed interval — a minimal in-process cron for scheduled work
4
+ * (digests, cleanup, cache warming).
5
+ *
6
+ * Deliberately simple: one `setInterval` per job, no drift compensation, no
7
+ * distributed locking, no retry/backoff. Fits a **single-process** deployment;
8
+ * for scale-out point a real scheduler (BullMQ, a platform cron) at the same
9
+ * endpoints instead. The runner starts idle — call `start()` explicitly.
10
+ *
11
+ * @param config - Secret/header, base URL, and the jobs to schedule.
12
+ * @returns A {@link CronRunner} handle (`start` / `stop` / `isRunning`).
13
+ * @example
14
+ * ```typescript
15
+ * // src/lib/server/cron.ts
16
+ * import { createCronRunner } from '@urbicon-ui/sveltekit-utils/cron';
17
+ * import { env } from '$env/dynamic/private';
18
+ *
19
+ * export const cron = createCronRunner({
20
+ * secret: env.CRON_SECRET,
21
+ * baseUrl: env.BASE_URL,
22
+ * jobs: [
23
+ * { path: '/api/cron/send-digest', intervalSeconds: 3600 },
24
+ * { path: '/api/cron/cleanup', intervalSeconds: 900 }
25
+ * ],
26
+ * onError: (job, err) => console.error(`Cron ${job.path} failed`, err)
27
+ * });
28
+ *
29
+ * cron.start();
30
+ * ```
31
+ */
1
32
  export function createCronRunner(config) {
2
33
  const timers = [];
3
34
  let running = false;
@@ -8,14 +39,26 @@ export function createCronRunner(config) {
8
39
  running = true;
9
40
  for (const job of config.jobs) {
10
41
  const timer = setInterval(async () => {
42
+ const base = config.baseUrl ?? 'http://localhost:3000';
43
+ let response;
11
44
  try {
12
- const base = config.baseUrl ?? 'http://localhost:3000';
13
- await fetch(`${base}${job.path}`, {
45
+ response = await fetch(`${base}${job.path}`, {
14
46
  method: job.method ?? 'POST',
15
47
  headers: { [config.secretHeader ?? 'x-cron-secret']: config.secret }
16
48
  });
17
49
  }
18
50
  catch (err) {
51
+ // Network-level failure (DNS, connection refused, abort): fetch rejected.
52
+ config.onError?.(job, err);
53
+ return;
54
+ }
55
+ // The request completed; a non-2xx status is still a failure. Handle it
56
+ // outside the try so a throwing `onError` escapes as an unhandled
57
+ // rejection rather than being re-caught and re-invoked here — symmetric
58
+ // with the rejection path above.
59
+ if (!response.ok) {
60
+ const err = new Error(`Cron job "${job.path}" returned ${response.status} ${response.statusText}`);
61
+ err.status = response.status;
19
62
  config.onError?.(job, err);
20
63
  }
21
64
  }, job.intervalSeconds * 1000);
@@ -1,19 +1,128 @@
1
1
  import { type TableQueryParams, type TableQueryUrlOptions } from './table-query';
2
+ /**
3
+ * How {@link useUrlArrayParam} maps an array onto the URL:
4
+ * - `repeat` — one entry per key: `?tag=a&tag=b`
5
+ * - `csv` — a single delimited value: `?tag=a,b`
6
+ */
2
7
  export type UrlArrayStrategy = 'repeat' | 'csv';
8
+ /** Codec + seed for {@link useUrlParam} / {@link createUrlParam}. */
3
9
  export type UrlParamOptions<T> = {
10
+ /**
11
+ * Read the value out of the current search params. Return `null`/`undefined`
12
+ * to signal "absent" — the getter then yields {@link initial}.
13
+ */
4
14
  parse: (sp: URLSearchParams) => T | null | undefined;
15
+ /**
16
+ * Encode the value into `URLSearchParams`. The keys it produces are the ones
17
+ * the setter manages: on write they are cleared from the current URL and
18
+ * replaced by this output, leaving every other param untouched. Emit no
19
+ * entry for a key to remove it from the URL.
20
+ */
5
21
  serialize: (value: T) => URLSearchParams;
22
+ /** Value the getter returns when {@link parse} yields `null`/`undefined`. */
6
23
  initial: T;
24
+ /**
25
+ * Replace the current history entry instead of pushing a new one, so rapid
26
+ * filter/pagination edits don't flood the back button.
27
+ * @default true
28
+ */
7
29
  replaceState?: boolean;
8
30
  };
31
+ /**
32
+ * Low-level escape hatch to update several params at once via `goto` (without a
33
+ * full navigation). Starts from the current URL, applies `next`, and keeps
34
+ * every unrelated param.
35
+ *
36
+ * Merge semantics per key in `next`: the key is first cleared, then re-applied
37
+ * — a `URLSearchParams` re-appends all of its entries (repeated keys survive),
38
+ * a record `set`s a scalar, `append`s each array element, and **removes** the
39
+ * key entirely for a `null`/`undefined` value.
40
+ *
41
+ * @param next - Params to apply, as `URLSearchParams` or a plain record. A
42
+ * record value of `null`/`undefined` deletes that key.
43
+ * @param opts - `replaceState` (default `true`) — replace vs. push history.
44
+ * @example
45
+ * ```typescript
46
+ * updateUrlSearchParams({ page: '1', tag: ['a', 'b'], filter: null });
47
+ * // ?page=1&tag=a&tag=b (any prior `filter` param is dropped)
48
+ * ```
49
+ */
9
50
  export declare function updateUrlSearchParams(next: URLSearchParams | Record<string, string | string[]>, opts?: {
10
51
  replaceState?: boolean;
11
52
  }): void;
53
+ /**
54
+ * Non-reactive core of {@link useUrlParam}: builds the `get(sp)` / `set(value)`
55
+ * pair without touching the `page` rune, so `get` can be evaluated against any
56
+ * `URLSearchParams`. Prefer {@link useUrlParam} in components — this is the
57
+ * escape hatch when you need to read against a snapshot other than the live
58
+ * page URL (tests, a server `load`).
59
+ *
60
+ * `set` rewrites only the keys that `options.serialize` produces (clear +
61
+ * re-append) and preserves the rest, then navigates with `goto`
62
+ * (`replaceState`, `noScroll`, `keepFocus`).
63
+ *
64
+ * @param _key - Ignored — `options.parse`/`options.serialize` already close
65
+ * over the key (see {@link useUrlArrayParam}); kept only for signature parity
66
+ * with {@link useUrlParam}.
67
+ * @param options - Parse/serialize codec, initial value, history behaviour.
68
+ * @returns `{ get, set }` — `get(sp)` reads a value from the given params
69
+ * (falling back to `initial`), `set(value)` writes it to the URL.
70
+ */
12
71
  export declare function createUrlParam<T>(_key: string, options: UrlParamOptions<T>): {
13
72
  readonly get: (sp: URLSearchParams) => T;
14
73
  readonly set: (next: T) => void;
15
74
  };
75
+ /**
76
+ * Bind a typed value to a URL search param, reactively. The returned getter
77
+ * reads through the `page` rune, so it re-evaluates whenever the URL changes;
78
+ * the setter writes the value back via `goto` (no full navigation).
79
+ *
80
+ * SSR-safe: the getter only reads `page.url` (populated on the server), so the
81
+ * initial render reflects the incoming URL. The setter calls the client-only
82
+ * `goto` and is meant to run from event handlers/effects — never during SSR.
83
+ *
84
+ * A **getter**, not a store, is returned on purpose: call it lazily inside
85
+ * `$derived`/`$effect` and the read is tracked there.
86
+ *
87
+ * @typeParam T - The decoded value type.
88
+ * @param key - Param key (forwarded to `createUrlParam` for signature parity;
89
+ * the actual key handling lives in `options.parse`/`options.serialize`).
90
+ * @param options - Parse/serialize codec, initial value, history behaviour.
91
+ * @returns `[get, set]` — `get()` reads the live value, `set(value)` writes it.
92
+ * @example
93
+ * ```svelte
94
+ * <script lang="ts">
95
+ * import { useUrlParam } from '@urbicon-ui/sveltekit-utils/url.svelte';
96
+ *
97
+ * const [page, setPage] = useUrlParam<number>('page', {
98
+ * parse: (sp) => Number(sp.get('page') ?? '1'),
99
+ * serialize: (v) => new URLSearchParams({ page: String(v) }),
100
+ * initial: 1
101
+ * });
102
+ * </script>
103
+ *
104
+ * <button onclick={() => setPage(page() + 1)}>Next — {page()}</button>
105
+ * ```
106
+ */
16
107
  export declare function useUrlParam<T>(key: string, options: UrlParamOptions<T>): readonly [() => T, (next: T) => void];
108
+ /**
109
+ * {@link useUrlParam} specialised for a `string[]`, with the encoding handled
110
+ * for you. Reactive read + `goto`-based write, same as {@link useUrlParam}.
111
+ *
112
+ * The `csv` strategy drops empty segments on read (`?tag=` → `[]`) and writes
113
+ * no param for an empty array, so an empty selection leaves the URL clean.
114
+ *
115
+ * @param key - The param key.
116
+ * @param opts - `initial` seed, `strategy` (default `'repeat'`), and
117
+ * `delimiter` for `csv` (default `','`). See {@link UrlArrayStrategy}.
118
+ * @returns `[get, set]` — `get()` reads the current `string[]`, `set(values)`
119
+ * writes it.
120
+ * @example
121
+ * ```typescript
122
+ * const [tags, setTags] = useUrlArrayParam('tag', { initial: [] }); // ?tag=a&tag=b
123
+ * const [cats, setCats] = useUrlArrayParam('cat', { initial: [], strategy: 'csv' }); // ?cat=a,b
124
+ * ```
125
+ */
17
126
  export declare function useUrlArrayParam(key: string, opts: {
18
127
  initial: string[];
19
128
  strategy?: UrlArrayStrategy;
@@ -1,6 +1,25 @@
1
1
  import { goto } from '$app/navigation';
2
2
  import { page } from '$app/state';
3
3
  import { applyTableQueryToSearchParams, searchParamsToTableQuery } from './table-query';
4
+ /**
5
+ * Low-level escape hatch to update several params at once via `goto` (without a
6
+ * full navigation). Starts from the current URL, applies `next`, and keeps
7
+ * every unrelated param.
8
+ *
9
+ * Merge semantics per key in `next`: the key is first cleared, then re-applied
10
+ * — a `URLSearchParams` re-appends all of its entries (repeated keys survive),
11
+ * a record `set`s a scalar, `append`s each array element, and **removes** the
12
+ * key entirely for a `null`/`undefined` value.
13
+ *
14
+ * @param next - Params to apply, as `URLSearchParams` or a plain record. A
15
+ * record value of `null`/`undefined` deletes that key.
16
+ * @param opts - `replaceState` (default `true`) — replace vs. push history.
17
+ * @example
18
+ * ```typescript
19
+ * updateUrlSearchParams({ page: '1', tag: ['a', 'b'], filter: null });
20
+ * // ?page=1&tag=a&tag=b (any prior `filter` param is dropped)
21
+ * ```
22
+ */
4
23
  // Local imperative use of URLSearchParams — not reactive state — so the
5
24
  // SvelteURLSearchParams wrapper is unnecessary here. Likewise for `goto`:
6
25
  // we pass constructed relative paths, not resolved route ids; callers of
@@ -33,6 +52,24 @@ export function updateUrlSearchParams(next, opts) {
33
52
  keepFocus: true
34
53
  });
35
54
  }
55
+ /**
56
+ * Non-reactive core of {@link useUrlParam}: builds the `get(sp)` / `set(value)`
57
+ * pair without touching the `page` rune, so `get` can be evaluated against any
58
+ * `URLSearchParams`. Prefer {@link useUrlParam} in components — this is the
59
+ * escape hatch when you need to read against a snapshot other than the live
60
+ * page URL (tests, a server `load`).
61
+ *
62
+ * `set` rewrites only the keys that `options.serialize` produces (clear +
63
+ * re-append) and preserves the rest, then navigates with `goto`
64
+ * (`replaceState`, `noScroll`, `keepFocus`).
65
+ *
66
+ * @param _key - Ignored — `options.parse`/`options.serialize` already close
67
+ * over the key (see {@link useUrlArrayParam}); kept only for signature parity
68
+ * with {@link useUrlParam}.
69
+ * @param options - Parse/serialize codec, initial value, history behaviour.
70
+ * @returns `{ get, set }` — `get(sp)` reads a value from the given params
71
+ * (falling back to `initial`), `set(value)` writes it to the URL.
72
+ */
36
73
  // `key` is unused here — `options.parse`/`options.serialize` already close over
37
74
  // it (see useUrlArrayParam) — but kept for signature parity with useUrlParam.
38
75
  export function createUrlParam(_key, options) {
@@ -53,11 +90,61 @@ export function createUrlParam(_key, options) {
53
90
  }
54
91
  return { get, set: setValue };
55
92
  }
93
+ /**
94
+ * Bind a typed value to a URL search param, reactively. The returned getter
95
+ * reads through the `page` rune, so it re-evaluates whenever the URL changes;
96
+ * the setter writes the value back via `goto` (no full navigation).
97
+ *
98
+ * SSR-safe: the getter only reads `page.url` (populated on the server), so the
99
+ * initial render reflects the incoming URL. The setter calls the client-only
100
+ * `goto` and is meant to run from event handlers/effects — never during SSR.
101
+ *
102
+ * A **getter**, not a store, is returned on purpose: call it lazily inside
103
+ * `$derived`/`$effect` and the read is tracked there.
104
+ *
105
+ * @typeParam T - The decoded value type.
106
+ * @param key - Param key (forwarded to `createUrlParam` for signature parity;
107
+ * the actual key handling lives in `options.parse`/`options.serialize`).
108
+ * @param options - Parse/serialize codec, initial value, history behaviour.
109
+ * @returns `[get, set]` — `get()` reads the live value, `set(value)` writes it.
110
+ * @example
111
+ * ```svelte
112
+ * <script lang="ts">
113
+ * import { useUrlParam } from '@urbicon-ui/sveltekit-utils/url.svelte';
114
+ *
115
+ * const [page, setPage] = useUrlParam<number>('page', {
116
+ * parse: (sp) => Number(sp.get('page') ?? '1'),
117
+ * serialize: (v) => new URLSearchParams({ page: String(v) }),
118
+ * initial: 1
119
+ * });
120
+ * </script>
121
+ *
122
+ * <button onclick={() => setPage(page() + 1)}>Next — {page()}</button>
123
+ * ```
124
+ */
56
125
  export function useUrlParam(key, options) {
57
126
  const { get, set } = createUrlParam(key, options);
58
127
  const getBound = () => get(page.url.searchParams);
59
128
  return [getBound, set];
60
129
  }
130
+ /**
131
+ * {@link useUrlParam} specialised for a `string[]`, with the encoding handled
132
+ * for you. Reactive read + `goto`-based write, same as {@link useUrlParam}.
133
+ *
134
+ * The `csv` strategy drops empty segments on read (`?tag=` → `[]`) and writes
135
+ * no param for an empty array, so an empty selection leaves the URL clean.
136
+ *
137
+ * @param key - The param key.
138
+ * @param opts - `initial` seed, `strategy` (default `'repeat'`), and
139
+ * `delimiter` for `csv` (default `','`). See {@link UrlArrayStrategy}.
140
+ * @returns `[get, set]` — `get()` reads the current `string[]`, `set(values)`
141
+ * writes it.
142
+ * @example
143
+ * ```typescript
144
+ * const [tags, setTags] = useUrlArrayParam('tag', { initial: [] }); // ?tag=a&tag=b
145
+ * const [cats, setCats] = useUrlArrayParam('cat', { initial: [], strategy: 'csv' }); // ?cat=a,b
146
+ * ```
147
+ */
61
148
  export function useUrlArrayParam(key, opts) {
62
149
  const strategy = opts.strategy ?? 'repeat';
63
150
  const delimiter = opts.delimiter ?? ',';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/sveltekit-utils",
3
- "version": "6.22.0",
3
+ "version": "6.24.0",
4
4
  "description": "SvelteKit helper utilities — createCronRunner and URL-state runes",
5
5
  "license": "MIT",
6
6
  "repository": {