@urbicon-ui/sveltekit-utils 7.0.1 → 8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/sveltekit-utils",
3
- "version": "7.0.1",
3
+ "version": "8.1.0",
4
4
  "description": "SvelteKit helper utilities — createCronRunner, streamSse, and URL-state runes",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -47,10 +47,10 @@
47
47
  "import": "./dist/sse.js",
48
48
  "default": "./dist/sse.js"
49
49
  },
50
- "./table-query": {
51
- "types": "./dist/table-query.d.ts",
52
- "import": "./dist/table-query.js",
53
- "default": "./dist/table-query.js"
50
+ "./table-view": {
51
+ "types": "./dist/table-view.d.ts",
52
+ "import": "./dist/table-view.js",
53
+ "default": "./dist/table-view.js"
54
54
  }
55
55
  },
56
56
  "files": [
@@ -1,204 +0,0 @@
1
- /**
2
- * URL (de)serialization for the query state a data table emits in server mode
3
- * (`TableQuery` from `@urbicon-ui/table`: page, page size, sort, search term,
4
- * column filters, grouping).
5
- *
6
- * The types in this module are a **structural mirror** of `TableQuery` — they
7
- * are deliberately not imported from `@urbicon-ui/table`, so this package
8
- * carries no dependency on the table package. Any object shaped like
9
- * `TableQuery` is accepted; a type-parity test in `@urbicon-ui/table` guards
10
- * the two shapes against drift.
11
- *
12
- * Serialization contract:
13
- * - **Deterministic** — fixed key order (`q`, `page`, `size`, `sort`, `dir`,
14
- * `group`, `filter`), stable filter order.
15
- * - **Default elision** — values equal to the resolved defaults are not
16
- * written; a table in its default state produces an empty query string.
17
- * - **Read tolerant** — unparsable params fall back to the defaults, and
18
- * malformed filter entries are skipped.
19
- * - **Write strict** — a structurally invalid query (non-positive page,
20
- * unknown filter operator, …) throws instead of writing corrupt state.
21
- */
22
- /** Sort direction of a table query. Mirrors `@urbicon-ui/table`. */
23
- export type TableQuerySortDirection = 'asc' | 'desc';
24
- /**
25
- * Filter operators supported by the table. Mirrors `FilterOperator` from
26
- * `@urbicon-ui/table`. Used as the runtime whitelist when parsing `filter`
27
- * params from the URL.
28
- */
29
- export declare const TABLE_QUERY_FILTER_OPERATORS: readonly ["contains", "equals", "startsWith", "endsWith", "greaterThan", "lessThan"];
30
- /** Filter operator of a table query filter. Mirrors `@urbicon-ui/table`. */
31
- export type TableQueryFilterOperator = (typeof TABLE_QUERY_FILTER_OPERATORS)[number];
32
- /** Single column filter of a table query. Mirrors `Filter` from `@urbicon-ui/table`. */
33
- export interface TableQueryFilter {
34
- /** Column ID the filter applies to. */
35
- column: string;
36
- /** Filter operator. */
37
- operator: TableQueryFilterOperator;
38
- /** Filter value (always a string, numeric operators convert internally). */
39
- value: string;
40
- }
41
- /**
42
- * Query state of a table in server mode. Structural mirror of `TableQuery`
43
- * from `@urbicon-ui/table` — the object passed to `queryFn` / `onQueryChange`
44
- * is directly assignable.
45
- */
46
- export interface TableQueryParams {
47
- /** Current page (1-based). */
48
- page: number;
49
- /** Number of items per page. */
50
- itemsPerPage: number;
51
- /** Column ID to sort by, or empty string if no sort is active. */
52
- sortColumn: string;
53
- /** Sort direction. */
54
- sortDirection: TableQuerySortDirection;
55
- /** Full-text search term. */
56
- searchTerm: string;
57
- /** Active column filters. */
58
- activeFilters: TableQueryFilter[];
59
- /** Column ID for grouping, or null if ungrouped. */
60
- groupByKey: string | null;
61
- }
62
- /**
63
- * The partial half of {@link TableQueryParams} — structural mirror of
64
- * `TableViewState` from `@urbicon-ui/table`, the type its `query` prop takes.
65
- *
66
- * Every field optional, and that is the whole contract: **presence means
67
- * controlled**. A consumer may hand over the sort and leave paging to the
68
- * table, so "absent" has to stay distinguishable from "set to its default
69
- * value" — which is exactly what {@link TableQueryParams} cannot express.
70
- */
71
- export interface TableQueryViewState {
72
- /** Current page (1-based). */
73
- page?: number;
74
- /** Number of items per page. */
75
- itemsPerPage?: number;
76
- /** Column ID to sort by, or empty string for no sort. */
77
- sortColumn?: string;
78
- /** Sort direction. */
79
- sortDirection?: TableQuerySortDirection;
80
- /** Full-text search term. */
81
- searchTerm?: string;
82
- /** Active column filters. */
83
- activeFilters?: TableQueryFilter[];
84
- /** Column ID for grouping, or null for ungrouped. */
85
- groupByKey?: string | null;
86
- }
87
- /**
88
- * Baseline used for default elision: query values equal to these defaults are
89
- * omitted from the URL, and missing params parse back to them.
90
- *
91
- * Set the defaults to the table's **initial, uncontrolled state** — i.e. the
92
- * values you pass as props (`itemsPerPage`, `initialPage`, `initialGroupBy`).
93
- * Unset fields fall back to the table's own defaults (page 1, 10 items per
94
- * page, no sort, empty search, ungrouped).
95
- */
96
- export interface TableQueryDefaults {
97
- /** Default page. @default 1 */
98
- page?: number;
99
- /** Default page size. @default 10 */
100
- itemsPerPage?: number;
101
- /** Default sort column ('' = unsorted). @default '' */
102
- sortColumn?: string;
103
- /** Default sort direction. @default 'asc' */
104
- sortDirection?: TableQuerySortDirection;
105
- /** Default search term. @default '' */
106
- searchTerm?: string;
107
- /** Default group key (null = ungrouped). @default null */
108
- groupByKey?: string | null;
109
- }
110
- /** Options shared by the table-query (de)serializers. */
111
- export interface TableQueryUrlOptions {
112
- /** Elision baseline — see {@link TableQueryDefaults}. */
113
- defaults?: TableQueryDefaults;
114
- /**
115
- * Prefix for every param key (`prefix: 't_'` → `?t_q=…&t_page=…`). Use it
116
- * to namespace multiple synced tables on the same page.
117
- * @default ''
118
- */
119
- prefix?: string;
120
- }
121
- /**
122
- * Serialize a table query into `URLSearchParams`, eliding every value that
123
- * equals the resolved defaults (see {@link TableQueryDefaults}).
124
- *
125
- * Key scheme (each key optionally prefixed via `options.prefix`):
126
- * - `q` — search term
127
- * - `page` — 1-based page
128
- * - `size` — items per page
129
- * - `sort` — sort column; an **empty** `sort=` marks "explicitly unsorted"
130
- * and is only written when the defaults specify a sort column
131
- * - `dir` — `desc` (ascending is implied when absent)
132
- * - `group` — group key; an empty `group=` marks "explicitly ungrouped"
133
- * - `filter` — repeated, `<column>:<operator>:<value>` with column and value
134
- * URI-component-encoded so the `:` separators stay unambiguous
135
- *
136
- * When `sortColumn` is empty the sort direction is meaningless and is
137
- * normalized away (it parses back as `'asc'`).
138
- *
139
- * Also handy for building the backend request inside `queryFn` — the same
140
- * scheme works as an API query string.
141
- *
142
- * @param query - Query emitted by the table (`TableQuery` is assignable).
143
- * @param options - Elision defaults + key prefix.
144
- * @returns Fresh `URLSearchParams` containing only non-default values.
145
- * @throws TypeError when the query is structurally invalid (write strict).
146
- */
147
- export declare function tableQueryToSearchParams(query: TableQueryParams, options?: TableQueryUrlOptions): URLSearchParams;
148
- /**
149
- * Parse `URLSearchParams` back into a full table query, filling every missing
150
- * param from the resolved defaults (see {@link TableQueryDefaults}).
151
- *
152
- * Read tolerant: non-numeric `page`/`size` fall back to the defaults, an
153
- * unknown `dir` becomes `'asc'`, and malformed `filter` entries (wrong shape,
154
- * unknown operator, broken percent-encoding) are skipped individually.
155
- *
156
- * Works anywhere a `URLSearchParams` exists — including `url.searchParams`
157
- * in a server `load`, to run the initial server-mode fetch during SSR.
158
- *
159
- * @param params - Search params to read (not mutated).
160
- * @param options - Fallback defaults + key prefix.
161
- * @returns Complete query object (assignable to `TableQuery`).
162
- */
163
- export declare function searchParamsToTableQuery(params: URLSearchParams, options?: TableQueryUrlOptions): TableQueryParams;
164
- /**
165
- * Parse `URLSearchParams` into the **partial** view state a table's `query`
166
- * prop takes — a key per axis the URL actually carries, and nothing else.
167
- *
168
- * This is the deliberate opposite of {@link searchParamsToTableQuery}, and the
169
- * distinction is load-bearing rather than cosmetic. The table reads `query` by
170
- * **field presence**: a present field means "this axis is controlled — outrank
171
- * persistence and the `initial*` seed"; an absent one means "the URL has no
172
- * opinion, carry on". A complete object therefore claims every axis, so
173
- * handing `searchParamsToTableQuery`'s output to `query` silently switches off
174
- * `persistenceConfig`, `initialSort`, `initialFilters` and `initialGroupBy` —
175
- * even on a URL with no params at all, where its every field is a default it
176
- * invented. That is not a hypothetical: it was the shipped wiring, and this
177
- * function exists because of it.
178
- *
179
- * `sortColumn` and `sortDirection` are emitted as a pair or not at all, so the
180
- * half-controlled sort (a direction with no column) cannot be produced here.
181
- *
182
- * Same tolerance as the full parser: an unparsable `page`/`size` falls back to
183
- * the resolved default *for that key* — the key was present, so the axis stays
184
- * controlled — and malformed filters are skipped individually.
185
- *
186
- * @param params - Search params to read (not mutated).
187
- * @param options - Fallback defaults + key prefix.
188
- * @returns Only the axes present in `params`.
189
- */
190
- export declare function searchParamsToTableViewState(params: URLSearchParams, options?: TableQueryUrlOptions): TableQueryViewState;
191
- /**
192
- * Merge a table query into existing search params: all managed keys (`q`,
193
- * `page`, `size`, `sort`, `dir`, `group`, `filter` — with the configured
194
- * prefix) are replaced by the serialized query, every other param is
195
- * preserved untouched. Managed keys whose value returned to the default are
196
- * removed (default elision).
197
- *
198
- * @param existing - Current search params (not mutated — a copy is returned).
199
- * @param query - Query emitted by the table.
200
- * @param options - Elision defaults + key prefix.
201
- * @returns New `URLSearchParams` with the query applied.
202
- * @throws TypeError when the query is structurally invalid (write strict).
203
- */
204
- export declare function applyTableQueryToSearchParams(existing: URLSearchParams, query: TableQueryParams, options?: TableQueryUrlOptions): URLSearchParams;
@@ -1,291 +0,0 @@
1
- /**
2
- * URL (de)serialization for the query state a data table emits in server mode
3
- * (`TableQuery` from `@urbicon-ui/table`: page, page size, sort, search term,
4
- * column filters, grouping).
5
- *
6
- * The types in this module are a **structural mirror** of `TableQuery` — they
7
- * are deliberately not imported from `@urbicon-ui/table`, so this package
8
- * carries no dependency on the table package. Any object shaped like
9
- * `TableQuery` is accepted; a type-parity test in `@urbicon-ui/table` guards
10
- * the two shapes against drift.
11
- *
12
- * Serialization contract:
13
- * - **Deterministic** — fixed key order (`q`, `page`, `size`, `sort`, `dir`,
14
- * `group`, `filter`), stable filter order.
15
- * - **Default elision** — values equal to the resolved defaults are not
16
- * written; a table in its default state produces an empty query string.
17
- * - **Read tolerant** — unparsable params fall back to the defaults, and
18
- * malformed filter entries are skipped.
19
- * - **Write strict** — a structurally invalid query (non-positive page,
20
- * unknown filter operator, …) throws instead of writing corrupt state.
21
- */
22
- /**
23
- * Filter operators supported by the table. Mirrors `FilterOperator` from
24
- * `@urbicon-ui/table`. Used as the runtime whitelist when parsing `filter`
25
- * params from the URL.
26
- */
27
- export const TABLE_QUERY_FILTER_OPERATORS = [
28
- 'contains',
29
- 'equals',
30
- 'startsWith',
31
- 'endsWith',
32
- 'greaterThan',
33
- 'lessThan'
34
- ];
35
- /** Resolved param key names for one prefix. */
36
- function paramKeys(prefix) {
37
- return {
38
- q: `${prefix}q`,
39
- page: `${prefix}page`,
40
- size: `${prefix}size`,
41
- sort: `${prefix}sort`,
42
- dir: `${prefix}dir`,
43
- group: `${prefix}group`,
44
- filter: `${prefix}filter`
45
- };
46
- }
47
- function resolveDefaults(defaults) {
48
- return {
49
- page: defaults?.page ?? 1,
50
- itemsPerPage: defaults?.itemsPerPage ?? 10,
51
- sortColumn: defaults?.sortColumn ?? '',
52
- sortDirection: defaults?.sortDirection ?? 'asc',
53
- searchTerm: defaults?.searchTerm ?? '',
54
- groupByKey: defaults?.groupByKey ?? null
55
- };
56
- }
57
- function isFilterOperator(value) {
58
- return TABLE_QUERY_FILTER_OPERATORS.includes(value);
59
- }
60
- /** Write-side validation: never serialize structurally invalid state. */
61
- function assertValidQuery(query) {
62
- if (!Number.isSafeInteger(query.page) || query.page < 1) {
63
- throw new TypeError(`[table-query] page must be a positive integer, got ${query.page}`);
64
- }
65
- if (!Number.isSafeInteger(query.itemsPerPage) || query.itemsPerPage < 1) {
66
- throw new TypeError(`[table-query] itemsPerPage must be a positive integer, got ${query.itemsPerPage}`);
67
- }
68
- if (query.sortDirection !== 'asc' && query.sortDirection !== 'desc') {
69
- throw new TypeError(`[table-query] sortDirection must be 'asc' or 'desc', got ${String(query.sortDirection)}`);
70
- }
71
- if (query.groupByKey === '') {
72
- throw new TypeError("[table-query] groupByKey must be a non-empty string or null, got ''");
73
- }
74
- for (const filter of query.activeFilters) {
75
- if (!filter.column) {
76
- throw new TypeError('[table-query] filter.column must be a non-empty string');
77
- }
78
- if (!isFilterOperator(filter.operator)) {
79
- throw new TypeError(`[table-query] unknown filter operator '${String(filter.operator)}' on column '${filter.column}'`);
80
- }
81
- }
82
- }
83
- /** Read-side tolerant integer parsing: anything non-numeric → null. */
84
- function parsePositiveInt(raw) {
85
- if (raw === null || !/^\d+$/.test(raw))
86
- return null;
87
- const value = Number(raw);
88
- return Number.isSafeInteger(value) && value >= 1 ? value : null;
89
- }
90
- /**
91
- * Parse one `filter` param value (`<column>:<operator>:<value>`, column and
92
- * value URI-component-encoded). Returns null for malformed entries — the
93
- * caller skips them (read tolerant).
94
- */
95
- function parseFilterParam(raw) {
96
- const parts = raw.split(':');
97
- if (parts.length !== 3)
98
- return null;
99
- const [encodedColumn, operator, encodedValue] = parts;
100
- if (!isFilterOperator(operator))
101
- return null;
102
- try {
103
- const column = decodeURIComponent(encodedColumn);
104
- if (!column)
105
- return null;
106
- return { column, operator, value: decodeURIComponent(encodedValue) };
107
- }
108
- catch {
109
- // Malformed percent-encoding (URIError) — skip the entry.
110
- return null;
111
- }
112
- }
113
- /**
114
- * Serialize a table query into `URLSearchParams`, eliding every value that
115
- * equals the resolved defaults (see {@link TableQueryDefaults}).
116
- *
117
- * Key scheme (each key optionally prefixed via `options.prefix`):
118
- * - `q` — search term
119
- * - `page` — 1-based page
120
- * - `size` — items per page
121
- * - `sort` — sort column; an **empty** `sort=` marks "explicitly unsorted"
122
- * and is only written when the defaults specify a sort column
123
- * - `dir` — `desc` (ascending is implied when absent)
124
- * - `group` — group key; an empty `group=` marks "explicitly ungrouped"
125
- * - `filter` — repeated, `<column>:<operator>:<value>` with column and value
126
- * URI-component-encoded so the `:` separators stay unambiguous
127
- *
128
- * When `sortColumn` is empty the sort direction is meaningless and is
129
- * normalized away (it parses back as `'asc'`).
130
- *
131
- * Also handy for building the backend request inside `queryFn` — the same
132
- * scheme works as an API query string.
133
- *
134
- * @param query - Query emitted by the table (`TableQuery` is assignable).
135
- * @param options - Elision defaults + key prefix.
136
- * @returns Fresh `URLSearchParams` containing only non-default values.
137
- * @throws TypeError when the query is structurally invalid (write strict).
138
- */
139
- export function tableQueryToSearchParams(query, options = {}) {
140
- assertValidQuery(query);
141
- const d = resolveDefaults(options.defaults);
142
- const k = paramKeys(options.prefix ?? '');
143
- const sp = new URLSearchParams();
144
- if (query.searchTerm !== d.searchTerm)
145
- sp.set(k.q, query.searchTerm);
146
- if (query.page !== d.page)
147
- sp.set(k.page, String(query.page));
148
- if (query.itemsPerPage !== d.itemsPerPage)
149
- sp.set(k.size, String(query.itemsPerPage));
150
- if (query.sortColumn === '') {
151
- // Unsorted: only mark explicitly when the defaults would re-introduce a sort.
152
- if (d.sortColumn !== '')
153
- sp.set(k.sort, '');
154
- }
155
- else if (query.sortColumn !== d.sortColumn || query.sortDirection !== d.sortDirection) {
156
- sp.set(k.sort, query.sortColumn);
157
- if (query.sortDirection === 'desc')
158
- sp.set(k.dir, 'desc');
159
- }
160
- if (query.groupByKey !== d.groupByKey)
161
- sp.set(k.group, query.groupByKey ?? '');
162
- for (const filter of query.activeFilters) {
163
- sp.append(k.filter, `${encodeURIComponent(filter.column)}:${filter.operator}:${encodeURIComponent(filter.value)}`);
164
- }
165
- return sp;
166
- }
167
- /**
168
- * Parse `URLSearchParams` back into a full table query, filling every missing
169
- * param from the resolved defaults (see {@link TableQueryDefaults}).
170
- *
171
- * Read tolerant: non-numeric `page`/`size` fall back to the defaults, an
172
- * unknown `dir` becomes `'asc'`, and malformed `filter` entries (wrong shape,
173
- * unknown operator, broken percent-encoding) are skipped individually.
174
- *
175
- * Works anywhere a `URLSearchParams` exists — including `url.searchParams`
176
- * in a server `load`, to run the initial server-mode fetch during SSR.
177
- *
178
- * @param params - Search params to read (not mutated).
179
- * @param options - Fallback defaults + key prefix.
180
- * @returns Complete query object (assignable to `TableQuery`).
181
- */
182
- export function searchParamsToTableQuery(params, options = {}) {
183
- const d = resolveDefaults(options.defaults);
184
- const k = paramKeys(options.prefix ?? '');
185
- let sortColumn = d.sortColumn;
186
- let sortDirection = d.sortDirection;
187
- const rawSort = params.get(k.sort);
188
- if (rawSort !== null) {
189
- sortColumn = rawSort;
190
- sortDirection = rawSort !== '' && params.get(k.dir) === 'desc' ? 'desc' : 'asc';
191
- }
192
- const rawGroup = params.get(k.group);
193
- const activeFilters = [];
194
- for (const raw of params.getAll(k.filter)) {
195
- const filter = parseFilterParam(raw);
196
- if (filter)
197
- activeFilters.push(filter);
198
- }
199
- return {
200
- page: parsePositiveInt(params.get(k.page)) ?? d.page,
201
- itemsPerPage: parsePositiveInt(params.get(k.size)) ?? d.itemsPerPage,
202
- sortColumn,
203
- sortDirection,
204
- searchTerm: params.get(k.q) ?? d.searchTerm,
205
- activeFilters,
206
- groupByKey: rawGroup !== null ? (rawGroup === '' ? null : rawGroup) : d.groupByKey
207
- };
208
- }
209
- /**
210
- * Parse `URLSearchParams` into the **partial** view state a table's `query`
211
- * prop takes — a key per axis the URL actually carries, and nothing else.
212
- *
213
- * This is the deliberate opposite of {@link searchParamsToTableQuery}, and the
214
- * distinction is load-bearing rather than cosmetic. The table reads `query` by
215
- * **field presence**: a present field means "this axis is controlled — outrank
216
- * persistence and the `initial*` seed"; an absent one means "the URL has no
217
- * opinion, carry on". A complete object therefore claims every axis, so
218
- * handing `searchParamsToTableQuery`'s output to `query` silently switches off
219
- * `persistenceConfig`, `initialSort`, `initialFilters` and `initialGroupBy` —
220
- * even on a URL with no params at all, where its every field is a default it
221
- * invented. That is not a hypothetical: it was the shipped wiring, and this
222
- * function exists because of it.
223
- *
224
- * `sortColumn` and `sortDirection` are emitted as a pair or not at all, so the
225
- * half-controlled sort (a direction with no column) cannot be produced here.
226
- *
227
- * Same tolerance as the full parser: an unparsable `page`/`size` falls back to
228
- * the resolved default *for that key* — the key was present, so the axis stays
229
- * controlled — and malformed filters are skipped individually.
230
- *
231
- * @param params - Search params to read (not mutated).
232
- * @param options - Fallback defaults + key prefix.
233
- * @returns Only the axes present in `params`.
234
- */
235
- export function searchParamsToTableViewState(params, options = {}) {
236
- const d = resolveDefaults(options.defaults);
237
- const k = paramKeys(options.prefix ?? '');
238
- const view = {};
239
- const rawPage = params.get(k.page);
240
- if (rawPage !== null)
241
- view.page = parsePositiveInt(rawPage) ?? d.page;
242
- const rawSize = params.get(k.size);
243
- if (rawSize !== null)
244
- view.itemsPerPage = parsePositiveInt(rawSize) ?? d.itemsPerPage;
245
- const rawSort = params.get(k.sort);
246
- if (rawSort !== null) {
247
- view.sortColumn = rawSort;
248
- view.sortDirection = rawSort !== '' && params.get(k.dir) === 'desc' ? 'desc' : 'asc';
249
- }
250
- const rawSearch = params.get(k.q);
251
- if (rawSearch !== null)
252
- view.searchTerm = rawSearch;
253
- const rawFilters = params.getAll(k.filter);
254
- if (rawFilters.length > 0) {
255
- const activeFilters = [];
256
- for (const raw of rawFilters) {
257
- const filter = parseFilterParam(raw);
258
- if (filter)
259
- activeFilters.push(filter);
260
- }
261
- view.activeFilters = activeFilters;
262
- }
263
- const rawGroup = params.get(k.group);
264
- if (rawGroup !== null)
265
- view.groupByKey = rawGroup === '' ? null : rawGroup;
266
- return view;
267
- }
268
- /**
269
- * Merge a table query into existing search params: all managed keys (`q`,
270
- * `page`, `size`, `sort`, `dir`, `group`, `filter` — with the configured
271
- * prefix) are replaced by the serialized query, every other param is
272
- * preserved untouched. Managed keys whose value returned to the default are
273
- * removed (default elision).
274
- *
275
- * @param existing - Current search params (not mutated — a copy is returned).
276
- * @param query - Query emitted by the table.
277
- * @param options - Elision defaults + key prefix.
278
- * @returns New `URLSearchParams` with the query applied.
279
- * @throws TypeError when the query is structurally invalid (write strict).
280
- */
281
- export function applyTableQueryToSearchParams(existing, query, options = {}) {
282
- const serialized = tableQueryToSearchParams(query, options);
283
- const next = new URLSearchParams(existing);
284
- for (const key of Object.values(paramKeys(options.prefix ?? ''))) {
285
- next.delete(key);
286
- }
287
- for (const [key, value] of serialized) {
288
- next.append(key, value);
289
- }
290
- return next;
291
- }