@cavuno/board 1.37.0 → 1.39.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.
@@ -0,0 +1,81 @@
1
+ import { S as Schemas } from './_spec-DxC1ze93.mjs';
2
+
3
+ type Awaitable<T> = T | Promise<T>;
4
+ /**
5
+ * Async token storage. The SDK reads the access token from storage on
6
+ * every request (fresh-token rule); auth methods write/clear the pair.
7
+ */
8
+ interface CustomStorage {
9
+ getItem(key: string): Awaitable<string | null>;
10
+ setItem(key: string, value: string): Awaitable<void>;
11
+ removeItem(key: string): Awaitable<void>;
12
+ }
13
+ type StorageMode = 'memory' | 'nostore' | 'local' | 'session' | CustomStorage;
14
+ declare const ACCESS_TOKEN_KEY = "cavuno_board_access_token";
15
+ declare const REFRESH_TOKEN_KEY = "cavuno_board_refresh_token";
16
+ /** Board-password access grant — sent as `X-Board-Access` to pass the wall. */
17
+ declare const BOARD_ACCESS_GRANT_KEY = "cavuno_board_access_grant";
18
+
19
+ /**
20
+ * The request handed to `onRequest` and `fetch`. `onRequest` may
21
+ * mutate or replace it wholesale (locale headers, URL rewrites);
22
+ * whatever it returns is what gets fetched.
23
+ */
24
+ interface BoardRequest {
25
+ /** Fully built URL, query string included. */
26
+ url: string;
27
+ /** Final RequestInit: method, Headers instance, serialized body, plus passthrough keys (`signal`, `cache`, `next`, `cf`, …). */
28
+ init: RequestInit;
29
+ }
30
+ interface Logger {
31
+ debug(message: string): void;
32
+ error(message: string): void;
33
+ }
34
+ /**
35
+ * Per-call options on every SDK method. Everything besides `body` and
36
+ * `query` is passed through to `fetch` untouched, so framework caching
37
+ * (`next: { tags }`, `cf: {...}`, `cache:`) rides for free.
38
+ */
39
+ type FetchOptions = Omit<RequestInit, 'body'> & {
40
+ body?: unknown;
41
+ query?: Record<string, unknown>;
42
+ };
43
+ interface BoardClientOptions {
44
+ baseUrl: string;
45
+ /** Board identifier: `pk_…` key | `boards_…` ID | slug (ADR-0032). */
46
+ board: string;
47
+ storage: CustomStorage;
48
+ globalHeaders?: Record<string, string>;
49
+ onRequest?: (req: BoardRequest) => Awaitable<BoardRequest>;
50
+ onResponse?: (res: Response, req: BoardRequest) => Awaitable<void>;
51
+ logger?: Logger;
52
+ }
53
+ declare class BoardClient {
54
+ readonly storage: CustomStorage;
55
+ private readonly basePath;
56
+ private readonly options;
57
+ constructor(options: BoardClientOptions);
58
+ /**
59
+ * The full request pipeline. Public and first-class: custom endpoints
60
+ * work without an SDK release, and still get the board-identifier
61
+ * base path, default headers, bearer token, and both hooks.
62
+ *
63
+ * @example
64
+ * const stats = await board.client.fetch<MyShape>('/custom/stats');
65
+ */
66
+ fetch<T>(path: string, init?: FetchOptions): Promise<T>;
67
+ }
68
+
69
+ type SuggestResult = Schemas['SuggestResult'];
70
+ type SuggestionItem = Schemas['SuggestionItem'];
71
+ type CompanySuggestion = Schemas['CompanySuggestion'];
72
+ type TermSuggestion = Schemas['TermSuggestion'];
73
+ /** Query for `board.search.suggest()`. */
74
+ type SearchSuggestQuery = {
75
+ /** Keyword; under 2 chars the server returns an empty `items` array. */
76
+ q?: string;
77
+ /** Max interleaved suggestions (1–25; default 25). */
78
+ limit?: number;
79
+ };
80
+
81
+ export { type Awaitable as A, type BoardRequest as B, type CompanySuggestion as C, type FetchOptions as F, type Logger as L, REFRESH_TOKEN_KEY as R, type SuggestionItem as S, type TermSuggestion as T, type SearchSuggestQuery as a, type SuggestResult as b, type StorageMode as c, BoardClient as d, ACCESS_TOKEN_KEY as e, BOARD_ACCESS_GRANT_KEY as f, type CustomStorage as g };
@@ -0,0 +1,81 @@
1
+ import { S as Schemas } from './_spec-DxC1ze93.js';
2
+
3
+ type Awaitable<T> = T | Promise<T>;
4
+ /**
5
+ * Async token storage. The SDK reads the access token from storage on
6
+ * every request (fresh-token rule); auth methods write/clear the pair.
7
+ */
8
+ interface CustomStorage {
9
+ getItem(key: string): Awaitable<string | null>;
10
+ setItem(key: string, value: string): Awaitable<void>;
11
+ removeItem(key: string): Awaitable<void>;
12
+ }
13
+ type StorageMode = 'memory' | 'nostore' | 'local' | 'session' | CustomStorage;
14
+ declare const ACCESS_TOKEN_KEY = "cavuno_board_access_token";
15
+ declare const REFRESH_TOKEN_KEY = "cavuno_board_refresh_token";
16
+ /** Board-password access grant — sent as `X-Board-Access` to pass the wall. */
17
+ declare const BOARD_ACCESS_GRANT_KEY = "cavuno_board_access_grant";
18
+
19
+ /**
20
+ * The request handed to `onRequest` and `fetch`. `onRequest` may
21
+ * mutate or replace it wholesale (locale headers, URL rewrites);
22
+ * whatever it returns is what gets fetched.
23
+ */
24
+ interface BoardRequest {
25
+ /** Fully built URL, query string included. */
26
+ url: string;
27
+ /** Final RequestInit: method, Headers instance, serialized body, plus passthrough keys (`signal`, `cache`, `next`, `cf`, …). */
28
+ init: RequestInit;
29
+ }
30
+ interface Logger {
31
+ debug(message: string): void;
32
+ error(message: string): void;
33
+ }
34
+ /**
35
+ * Per-call options on every SDK method. Everything besides `body` and
36
+ * `query` is passed through to `fetch` untouched, so framework caching
37
+ * (`next: { tags }`, `cf: {...}`, `cache:`) rides for free.
38
+ */
39
+ type FetchOptions = Omit<RequestInit, 'body'> & {
40
+ body?: unknown;
41
+ query?: Record<string, unknown>;
42
+ };
43
+ interface BoardClientOptions {
44
+ baseUrl: string;
45
+ /** Board identifier: `pk_…` key | `boards_…` ID | slug (ADR-0032). */
46
+ board: string;
47
+ storage: CustomStorage;
48
+ globalHeaders?: Record<string, string>;
49
+ onRequest?: (req: BoardRequest) => Awaitable<BoardRequest>;
50
+ onResponse?: (res: Response, req: BoardRequest) => Awaitable<void>;
51
+ logger?: Logger;
52
+ }
53
+ declare class BoardClient {
54
+ readonly storage: CustomStorage;
55
+ private readonly basePath;
56
+ private readonly options;
57
+ constructor(options: BoardClientOptions);
58
+ /**
59
+ * The full request pipeline. Public and first-class: custom endpoints
60
+ * work without an SDK release, and still get the board-identifier
61
+ * base path, default headers, bearer token, and both hooks.
62
+ *
63
+ * @example
64
+ * const stats = await board.client.fetch<MyShape>('/custom/stats');
65
+ */
66
+ fetch<T>(path: string, init?: FetchOptions): Promise<T>;
67
+ }
68
+
69
+ type SuggestResult = Schemas['SuggestResult'];
70
+ type SuggestionItem = Schemas['SuggestionItem'];
71
+ type CompanySuggestion = Schemas['CompanySuggestion'];
72
+ type TermSuggestion = Schemas['TermSuggestion'];
73
+ /** Query for `board.search.suggest()`. */
74
+ type SearchSuggestQuery = {
75
+ /** Keyword; under 2 chars the server returns an empty `items` array. */
76
+ q?: string;
77
+ /** Max interleaved suggestions (1–25; default 25). */
78
+ limit?: number;
79
+ };
80
+
81
+ export { type Awaitable as A, type BoardRequest as B, type CompanySuggestion as C, type FetchOptions as F, type Logger as L, REFRESH_TOKEN_KEY as R, type SuggestionItem as S, type TermSuggestion as T, type SearchSuggestQuery as a, type SuggestResult as b, type StorageMode as c, BoardClient as d, ACCESS_TOKEN_KEY as e, BOARD_ACCESS_GRANT_KEY as f, type CustomStorage as g };
package/dist/seo.d.mts CHANGED
@@ -1,7 +1,8 @@
1
- import { P as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, a as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-9U42CM5A.mjs';
2
- import { P as PublicBoard } from './board-D0guOBYy.mjs';
3
- import { P as PublicJob } from './jobs-CMCADU_-.mjs';
1
+ import { P as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, a as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-Rb5h_eVZ.mjs';
2
+ import { P as PublicBoard } from './board-CqYibYUA.mjs';
3
+ import { P as PublicJob } from './jobs-DPPA1Nev.mjs';
4
4
  import { B as BoardLabelOverrides } from './ui-copy-CKfFTtLk.mjs';
5
+ import './_spec-DxC1ze93.mjs';
5
6
 
6
7
  /**
7
8
  * Google for Jobs `JobPosting` structured data on the `@cavuno/board` wire
package/dist/seo.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { P as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, a as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-C3w9kvPJ.js';
2
- import { P as PublicBoard } from './board-D_BRa2zC.js';
3
- import { P as PublicJob } from './jobs-CMCADU_-.js';
1
+ import { P as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, a as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-DK4RnJnw.js';
2
+ import { P as PublicBoard } from './board-RfZEAJse.js';
3
+ import { P as PublicJob } from './jobs-CLLIvtMc.js';
4
4
  import { B as BoardLabelOverrides } from './ui-copy-CKfFTtLk.js';
5
+ import './_spec-DxC1ze93.js';
5
6
 
6
7
  /**
7
8
  * Google for Jobs `JobPosting` structured data on the `@cavuno/board` wire
package/dist/server.d.mts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './jobs-CMCADU_-.mjs';
3
- import './board-D0guOBYy.mjs';
4
- import './salaries-9U42CM5A.mjs';
2
+ import './_spec-DxC1ze93.mjs';
3
+ import './search-CqBa1Qc4.mjs';
4
+ import './board-CqYibYUA.mjs';
5
+ import './jobs-DPPA1Nev.mjs';
6
+ import './salaries-Rb5h_eVZ.mjs';
5
7
 
6
8
  /**
7
9
  * Session cookie codec — pure (no framework imports, no node imports) so it
package/dist/server.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './jobs-CMCADU_-.js';
3
- import './board-D_BRa2zC.js';
4
- import './salaries-C3w9kvPJ.js';
2
+ import './_spec-DxC1ze93.js';
3
+ import './search-DBoMM-gE.js';
4
+ import './board-RfZEAJse.js';
5
+ import './jobs-CLLIvtMc.js';
6
+ import './salaries-DK4RnJnw.js';
5
7
 
6
8
  /**
7
9
  * Session cookie codec — pure (no framework imports, no node imports) so it
@@ -1,7 +1,9 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './jobs-CMCADU_-.mjs';
3
- import './board-D0guOBYy.mjs';
4
- import './salaries-9U42CM5A.mjs';
2
+ import './_spec-DxC1ze93.mjs';
3
+ import './search-CqBa1Qc4.mjs';
4
+ import './board-CqYibYUA.mjs';
5
+ import './jobs-DPPA1Nev.mjs';
6
+ import './salaries-Rb5h_eVZ.mjs';
5
7
 
6
8
  /**
7
9
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
package/dist/sitemap.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './jobs-CMCADU_-.js';
3
- import './board-D_BRa2zC.js';
4
- import './salaries-C3w9kvPJ.js';
2
+ import './_spec-DxC1ze93.js';
3
+ import './search-DBoMM-gE.js';
4
+ import './board-RfZEAJse.js';
5
+ import './jobs-CLLIvtMc.js';
6
+ import './salaries-DK4RnJnw.js';
5
7
 
6
8
  /**
7
9
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
@@ -0,0 +1,70 @@
1
+ import { S as SuggestionItem, a as SearchSuggestQuery, F as FetchOptions, b as SuggestResult } from './search-CqBa1Qc4.mjs';
2
+ import './_spec-DxC1ze93.mjs';
3
+
4
+ /**
5
+ * `@cavuno/board/suggest` — framework-agnostic headless search-suggest
6
+ * controller (Algolia autocomplete-core pattern).
7
+ *
8
+ * Owns debounce, abort, and stale-drop so tenant frontends don't re-invent
9
+ * them. Patches ship via npm update. Zero runtime deps; isomorphic
10
+ * (`globalThis` timers only — never `window`/`document`).
11
+ *
12
+ * The ONE permitted view-level filter is excluding currently-applied company
13
+ * slugs from results (type `company` only). Wire data is never reshaped
14
+ * otherwise — order stays server-ranked.
15
+ */
16
+
17
+ interface SuggestControllerOptions {
18
+ /** Minimum characters before querying (default 2 — hosted parity). */
19
+ minChars?: number;
20
+ /** Debounce for setQuery → request (default 250ms). */
21
+ debounceMs?: number;
22
+ /** Server `limit` passed through (1–25). */
23
+ limit?: number;
24
+ }
25
+ type SuggestStatus = 'idle' | 'loading' | 'ready' | 'error';
26
+ interface SuggestState {
27
+ readonly query: string;
28
+ /**
29
+ * Server-ranked items. Previous items are RETAINED while a new request
30
+ * is loading (no flash-to-empty — hosted behavior).
31
+ */
32
+ readonly items: readonly SuggestionItem[];
33
+ /**
34
+ * `'loading'` from the moment a qualifying query is set (debounce window
35
+ * included) until that query's request settles. `'idle'` only when the
36
+ * query is empty/below minChars. `'ready'` / `'error'` after settle.
37
+ */
38
+ readonly status: SuggestStatus;
39
+ readonly error: unknown;
40
+ }
41
+ interface SuggestController {
42
+ setQuery(query: string): void;
43
+ /** Company slugs to hide from results (e.g. the currently-applied filter). */
44
+ setExcludedCompanySlugs(slugs: readonly string[]): void;
45
+ getState(): SuggestState;
46
+ /** Returns an unsubscribe function. Compatible with useSyncExternalStore. */
47
+ subscribe(listener: () => void): () => void;
48
+ dispose(): void;
49
+ }
50
+ type SuggestBoard = {
51
+ search: {
52
+ suggest(q?: SearchSuggestQuery, o?: FetchOptions): Promise<SuggestResult>;
53
+ };
54
+ };
55
+ /**
56
+ * Create a headless suggest controller bound to a board client (or any
57
+ * object exposing `search.suggest`).
58
+ *
59
+ * @example
60
+ * import { createBoardClient } from '@cavuno/board';
61
+ * import { createSuggestController } from '@cavuno/board/suggest';
62
+ *
63
+ * const board = createBoardClient({ board: 'pk_…' });
64
+ * const suggest = createSuggestController(board);
65
+ * suggest.subscribe(() => render(suggest.getState()));
66
+ * suggest.setQuery('acme');
67
+ */
68
+ declare function createSuggestController(board: SuggestBoard, options?: SuggestControllerOptions): SuggestController;
69
+
70
+ export { type SuggestController, type SuggestControllerOptions, type SuggestState, type SuggestStatus, createSuggestController };
@@ -0,0 +1,70 @@
1
+ import { S as SuggestionItem, a as SearchSuggestQuery, F as FetchOptions, b as SuggestResult } from './search-DBoMM-gE.js';
2
+ import './_spec-DxC1ze93.js';
3
+
4
+ /**
5
+ * `@cavuno/board/suggest` — framework-agnostic headless search-suggest
6
+ * controller (Algolia autocomplete-core pattern).
7
+ *
8
+ * Owns debounce, abort, and stale-drop so tenant frontends don't re-invent
9
+ * them. Patches ship via npm update. Zero runtime deps; isomorphic
10
+ * (`globalThis` timers only — never `window`/`document`).
11
+ *
12
+ * The ONE permitted view-level filter is excluding currently-applied company
13
+ * slugs from results (type `company` only). Wire data is never reshaped
14
+ * otherwise — order stays server-ranked.
15
+ */
16
+
17
+ interface SuggestControllerOptions {
18
+ /** Minimum characters before querying (default 2 — hosted parity). */
19
+ minChars?: number;
20
+ /** Debounce for setQuery → request (default 250ms). */
21
+ debounceMs?: number;
22
+ /** Server `limit` passed through (1–25). */
23
+ limit?: number;
24
+ }
25
+ type SuggestStatus = 'idle' | 'loading' | 'ready' | 'error';
26
+ interface SuggestState {
27
+ readonly query: string;
28
+ /**
29
+ * Server-ranked items. Previous items are RETAINED while a new request
30
+ * is loading (no flash-to-empty — hosted behavior).
31
+ */
32
+ readonly items: readonly SuggestionItem[];
33
+ /**
34
+ * `'loading'` from the moment a qualifying query is set (debounce window
35
+ * included) until that query's request settles. `'idle'` only when the
36
+ * query is empty/below minChars. `'ready'` / `'error'` after settle.
37
+ */
38
+ readonly status: SuggestStatus;
39
+ readonly error: unknown;
40
+ }
41
+ interface SuggestController {
42
+ setQuery(query: string): void;
43
+ /** Company slugs to hide from results (e.g. the currently-applied filter). */
44
+ setExcludedCompanySlugs(slugs: readonly string[]): void;
45
+ getState(): SuggestState;
46
+ /** Returns an unsubscribe function. Compatible with useSyncExternalStore. */
47
+ subscribe(listener: () => void): () => void;
48
+ dispose(): void;
49
+ }
50
+ type SuggestBoard = {
51
+ search: {
52
+ suggest(q?: SearchSuggestQuery, o?: FetchOptions): Promise<SuggestResult>;
53
+ };
54
+ };
55
+ /**
56
+ * Create a headless suggest controller bound to a board client (or any
57
+ * object exposing `search.suggest`).
58
+ *
59
+ * @example
60
+ * import { createBoardClient } from '@cavuno/board';
61
+ * import { createSuggestController } from '@cavuno/board/suggest';
62
+ *
63
+ * const board = createBoardClient({ board: 'pk_…' });
64
+ * const suggest = createSuggestController(board);
65
+ * suggest.subscribe(() => render(suggest.getState()));
66
+ * suggest.setQuery('acme');
67
+ */
68
+ declare function createSuggestController(board: SuggestBoard, options?: SuggestControllerOptions): SuggestController;
69
+
70
+ export { type SuggestController, type SuggestControllerOptions, type SuggestState, type SuggestStatus, createSuggestController };
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/suggest/index.ts
21
+ var suggest_exports = {};
22
+ __export(suggest_exports, {
23
+ createSuggestController: () => createSuggestController
24
+ });
25
+ module.exports = __toCommonJS(suggest_exports);
26
+ var DEFAULT_MIN_CHARS = 2;
27
+ var DEFAULT_DEBOUNCE_MS = 250;
28
+ function isAbortError(error) {
29
+ return typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
30
+ }
31
+ function applyExclusions(items, excluded) {
32
+ if (excluded.size === 0) return items.slice();
33
+ return items.filter(
34
+ (item) => !(item.type === "company" && excluded.has(item.slug.toLowerCase()))
35
+ );
36
+ }
37
+ function createSuggestController(board, options) {
38
+ const minChars = options?.minChars ?? DEFAULT_MIN_CHARS;
39
+ const debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;
40
+ const limit = options?.limit;
41
+ let disposed = false;
42
+ let state = {
43
+ query: "",
44
+ items: [],
45
+ status: "idle",
46
+ error: null
47
+ };
48
+ let rawItems = [];
49
+ let excluded = /* @__PURE__ */ new Set();
50
+ let sequence = 0;
51
+ let timer = null;
52
+ let abortController = null;
53
+ const listeners = /* @__PURE__ */ new Set();
54
+ function notify() {
55
+ for (const listener of listeners) listener();
56
+ }
57
+ function setState(next) {
58
+ state = next;
59
+ notify();
60
+ }
61
+ function cancelTimer() {
62
+ if (timer !== null) {
63
+ globalThis.clearTimeout(timer);
64
+ timer = null;
65
+ }
66
+ }
67
+ function abortInFlight() {
68
+ if (abortController) {
69
+ abortController.abort();
70
+ abortController = null;
71
+ }
72
+ }
73
+ function runRequest(query) {
74
+ const seq = ++sequence;
75
+ abortInFlight();
76
+ const controller = new AbortController();
77
+ abortController = controller;
78
+ const suggestQuery = { q: query };
79
+ if (limit !== void 0) suggestQuery.limit = limit;
80
+ void board.search.suggest(suggestQuery, { signal: controller.signal }).then((result) => {
81
+ if (disposed || seq !== sequence) return;
82
+ abortController = null;
83
+ rawItems = result.items;
84
+ setState({
85
+ query: state.query,
86
+ items: applyExclusions(rawItems, excluded),
87
+ status: "ready",
88
+ error: null
89
+ });
90
+ }).catch((error) => {
91
+ if (disposed || seq !== sequence) return;
92
+ if (isAbortError(error)) return;
93
+ abortController = null;
94
+ setState({
95
+ query: state.query,
96
+ items: state.items,
97
+ status: "error",
98
+ error
99
+ });
100
+ });
101
+ }
102
+ return {
103
+ setQuery(query) {
104
+ if (disposed) return;
105
+ const trimmed = query.trim();
106
+ cancelTimer();
107
+ if (trimmed.length < minChars) {
108
+ sequence += 1;
109
+ abortInFlight();
110
+ rawItems = [];
111
+ setState({
112
+ query,
113
+ items: [],
114
+ status: "idle",
115
+ error: null
116
+ });
117
+ return;
118
+ }
119
+ sequence += 1;
120
+ abortInFlight();
121
+ setState({
122
+ query,
123
+ items: state.items,
124
+ status: "loading",
125
+ error: null
126
+ });
127
+ timer = globalThis.setTimeout(() => {
128
+ timer = null;
129
+ if (disposed) return;
130
+ runRequest(trimmed);
131
+ }, debounceMs);
132
+ },
133
+ setExcludedCompanySlugs(slugs) {
134
+ if (disposed) return;
135
+ excluded = new Set(
136
+ slugs.map((s) => s.trim().toLowerCase()).filter(Boolean)
137
+ );
138
+ setState({
139
+ query: state.query,
140
+ items: applyExclusions(rawItems, excluded),
141
+ status: state.status,
142
+ error: state.error
143
+ });
144
+ },
145
+ getState() {
146
+ return state;
147
+ },
148
+ subscribe(listener) {
149
+ if (disposed) return () => {
150
+ };
151
+ listeners.add(listener);
152
+ return () => {
153
+ listeners.delete(listener);
154
+ };
155
+ },
156
+ dispose() {
157
+ if (disposed) return;
158
+ disposed = true;
159
+ cancelTimer();
160
+ abortInFlight();
161
+ sequence += 1;
162
+ listeners.clear();
163
+ }
164
+ };
165
+ }