nuxt-ai-ready 2.0.1 → 2.2.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.
Files changed (57) hide show
  1. package/dist/chunks/agent-skills.mjs +4 -1
  2. package/dist/chunks/prerender.mjs +10 -3
  3. package/dist/cli.mjs +67 -11
  4. package/dist/module.d.mts +3 -1
  5. package/dist/module.json +1 -1
  6. package/dist/module.mjs +4 -1
  7. package/dist/runtime/app/plugins/md-alternate.server.js +6 -2
  8. package/dist/runtime/llms-txt-utils.js +2 -2
  9. package/dist/runtime/markdown-path.d.ts +2 -0
  10. package/dist/runtime/markdown-path.js +4 -0
  11. package/dist/runtime/server/db/context.d.ts +2 -0
  12. package/dist/runtime/server/db/context.js +2 -0
  13. package/dist/runtime/server/db/drizzle/client.d.ts +6 -1
  14. package/dist/runtime/server/db/drizzle/client.js +66 -13
  15. package/dist/runtime/server/db/drizzle/index.d.ts +2 -2
  16. package/dist/runtime/server/db/drizzle/index.js +1 -2
  17. package/dist/runtime/server/db/drizzle/providers/dbPath.js +1 -1
  18. package/dist/runtime/server/db/drizzle/providers/postgres.d.ts +8 -0
  19. package/dist/runtime/server/db/drizzle/providers/postgres.js +17 -0
  20. package/dist/runtime/server/db/drizzle/queries.d.ts +1 -5
  21. package/dist/runtime/server/db/drizzle/queries.js +79 -38
  22. package/dist/runtime/server/db/drizzle/raw.d.ts +2 -2
  23. package/dist/runtime/server/db/drizzle/raw.js +32 -13
  24. package/dist/runtime/server/db/queries.d.ts +30 -27
  25. package/dist/runtime/server/db/queries.js +142 -91
  26. package/dist/runtime/server/db/schema/postgres.d.ts +20 -20
  27. package/dist/runtime/server/db/schema/postgres.js +6 -6
  28. package/dist/runtime/server/db/shared.d.ts +16 -0
  29. package/dist/runtime/server/db/shared.js +12 -0
  30. package/dist/runtime/server/middleware/markdown.js +10 -5
  31. package/dist/runtime/server/middleware/markdown.prerender.js +4 -1
  32. package/dist/runtime/server/plugins/db-lifecycle.js +5 -4
  33. package/dist/runtime/server/plugins/sitemap-seeder.js +9 -4
  34. package/dist/runtime/server/routes/__ai-ready/cron.get.js +10 -2
  35. package/dist/runtime/server/routes/__ai-ready/poll.post.js +30 -14
  36. package/dist/runtime/server/routes/__ai-ready/reindex.post.d.ts +2 -0
  37. package/dist/runtime/server/routes/__ai-ready/reindex.post.js +23 -0
  38. package/dist/runtime/server/routes/api-catalog.js +11 -2
  39. package/dist/runtime/server/routes/llms.txt.get.js +17 -12
  40. package/dist/runtime/server/routes/sitemap.md.get.d.ts +2 -0
  41. package/dist/runtime/server/routes/sitemap.md.get.js +51 -0
  42. package/dist/runtime/server/tasks/ai-ready-cron.js +3 -0
  43. package/dist/runtime/server/utils/i18n.d.ts +4 -0
  44. package/dist/runtime/server/utils/i18n.js +13 -0
  45. package/dist/runtime/server/utils/indexPage.d.ts +0 -1
  46. package/dist/runtime/server/utils/indexPage.js +10 -4
  47. package/dist/runtime/server/utils/link-header.d.ts +2 -0
  48. package/dist/runtime/server/utils/link-header.js +6 -0
  49. package/dist/runtime/server/utils/markdown-request.js +3 -1
  50. package/dist/runtime/server/utils/negotiation-decision.d.ts +1 -1
  51. package/dist/runtime/server/utils/runCron.d.ts +24 -0
  52. package/dist/runtime/server/utils/runCron.js +21 -4
  53. package/dist/runtime/server/utils/sitemap-md.d.ts +13 -0
  54. package/dist/runtime/server/utils/sitemap-md.js +65 -0
  55. package/dist/runtime/types.d.ts +21 -7
  56. package/dist/shared/{nuxt-ai-ready.D8qbnNrv.mjs → nuxt-ai-ready.D5URd0TD.mjs} +107 -39
  57. package/package.json +31 -26
@@ -1,10 +1,11 @@
1
- import { and, count, desc, eq, gt, isNotNull, isNull, like, lt, or, sql } from "drizzle-orm";
1
+ import { and, count, desc, eq, gt, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
2
2
  import { cronRuns, info, pages, sitemaps } from "#ai-ready-virtual/db-schema.mjs";
3
3
  import { useRuntimeConfig } from "#nuxtseo/nitro";
4
4
  import { parseSitemapCrawlState, serializeSitemapCrawlState } from "../../utils/sitemap-crawl-state.js";
5
5
  import { resolveFtsTokenizer as validateFtsTokenizer } from "../schema-sql.js";
6
+ import { LIKE_ESCAPE, likeSubstring, maxRowsPerInsert } from "../shared.js";
6
7
  import { useDrizzle } from "./client.js";
7
- import { useRawDb } from "./raw.js";
8
+ import { getRawExecutor, useRawDb } from "./raw.js";
8
9
  function resolveFtsTokenizer(event) {
9
10
  const cfg = useRuntimeConfig(event);
10
11
  return validateFtsTokenizer(cfg["nuxt-ai-ready"]?.ftsTokenizer);
@@ -127,7 +128,8 @@ export async function getPageLastmods(event) {
127
128
  export async function searchPages(event, query, options) {
128
129
  const client = await useDrizzle(event);
129
130
  const limitNum = options?.limit || 20;
130
- const searchTerm = `%${query}%`;
131
+ const searchTerm = likeSubstring(query);
132
+ const matches = (col) => sql`${col} LIKE ${searchTerm} ESCAPE ${LIKE_ESCAPE}`;
131
133
  if (client.dialect === "sqlite") {
132
134
  try {
133
135
  const ftsResult = await client.db.all(sql`
@@ -155,10 +157,10 @@ export async function searchPages(event, query, options) {
155
157
  and(
156
158
  eq(pages.isError, 0),
157
159
  or(
158
- like(pages.title, searchTerm),
159
- like(pages.description, searchTerm),
160
- like(pages.markdown, searchTerm),
161
- like(pages.headings, searchTerm)
160
+ matches(pages.title),
161
+ matches(pages.description),
162
+ matches(pages.markdown),
163
+ matches(pages.headings)
162
164
  )
163
165
  )
164
166
  ).limit(limitNum);
@@ -172,6 +174,8 @@ export async function countPages(event, options) {
172
174
  }
173
175
  if (options?.errors !== void 0) {
174
176
  conditions.push(eq(pages.isError, options.errors ? 1 : 0));
177
+ } else {
178
+ conditions.push(eq(pages.isError, 0));
175
179
  }
176
180
  let query = client.db.select({ count: count() }).from(pages);
177
181
  if (conditions.length > 0) {
@@ -180,13 +184,12 @@ export async function countPages(event, options) {
180
184
  const result = await query;
181
185
  return Number(result[0]?.count || 0);
182
186
  }
183
- export async function deletePage(event, route) {
184
- const client = await useDrizzle(event);
185
- await client.db.delete(pages).where(eq(pages.route, route));
186
- }
187
187
  export async function getPendingPages(event, limit = 10) {
188
188
  const client = await useDrizzle(event);
189
- return client.db.select({ route: pages.route }).from(pages).where(eq(pages.indexed, 0)).limit(limit);
189
+ return client.db.select({ route: pages.route }).from(pages).where(and(
190
+ eq(pages.indexed, 0),
191
+ eq(pages.isError, 0)
192
+ )).orderBy(pages.route).limit(limit);
190
193
  }
191
194
  export async function markPageIndexed(event, route) {
192
195
  const client = await useDrizzle(event);
@@ -202,7 +205,7 @@ export async function markRoutesPending(event, routes) {
202
205
  const batch = routes.slice(i, i + D1_MAX_IN_ROUTES);
203
206
  const placeholders = batch.map(() => "?").join(",");
204
207
  stmts.push({
205
- sql: `UPDATE ai_ready_pages SET indexed = 0 WHERE route IN (${placeholders})`,
208
+ sql: `UPDATE ai_ready_pages SET indexed = 0, is_error = 0 WHERE route IN (${placeholders})`,
206
209
  params: batch
207
210
  });
208
211
  }
@@ -229,16 +232,23 @@ export async function deleteInfoValue(event, key) {
229
232
  const client = await useDrizzle(event);
230
233
  await client.db.delete(info).where(eq(info.id, key));
231
234
  }
232
- const SCHEMA_VERSION = "v2.3.0-drizzle";
235
+ const SQLITE_SCHEMA_VERSION = "v2.3.0-drizzle";
236
+ const LEGACY_POSTGRES_INTEGER_SCHEMA_VERSION = "v2.3.0-drizzle";
237
+ const POSTGRES_SCHEMA_VERSION = "v2.3.0-drizzle-postgres-bigint";
233
238
  export async function initSchema(event) {
234
239
  const client = await useDrizzle(event);
235
240
  const tokenizer = resolveFtsTokenizer(event);
241
+ const schemaVersion = client.dialect === "postgres" ? POSTGRES_SCHEMA_VERSION : SQLITE_SCHEMA_VERSION;
236
242
  const currentVersion = await getSchemaVersion(client);
237
243
  const currentTokenizer = client.dialect === "postgres" ? null : await getStoredTokenizer(client);
238
- const versionMatches = currentVersion === SCHEMA_VERSION;
244
+ const versionMatches = currentVersion === schemaVersion;
239
245
  const tokenizerMatches = client.dialect === "postgres" || !currentTokenizer || currentTokenizer === tokenizer;
240
246
  if (versionMatches && tokenizerMatches)
241
247
  return;
248
+ if (client.dialect === "postgres" && currentVersion === LEGACY_POSTGRES_INTEGER_SCHEMA_VERSION) {
249
+ await migratePostgresIntegerTimestamps(client);
250
+ return;
251
+ }
242
252
  if (currentVersion) {
243
253
  if (client.dialect === "postgres") {
244
254
  await dropPostgresTables(client);
@@ -251,9 +261,9 @@ export async function initSchema(event) {
251
261
  } else {
252
262
  await createSQLiteTables(client, tokenizer);
253
263
  }
254
- await client.db.insert(info).values({ id: "schema", version: SCHEMA_VERSION }).onConflictDoUpdate({
264
+ await client.db.insert(info).values({ id: "schema", version: schemaVersion }).onConflictDoUpdate({
255
265
  target: info.id,
256
- set: { version: SCHEMA_VERSION }
266
+ set: { version: schemaVersion }
257
267
  });
258
268
  if (client.dialect !== "postgres") {
259
269
  await client.db.insert(info).values({ id: "fts_tokenizer", value: tokenizer }).onConflictDoUpdate({
@@ -264,23 +274,38 @@ export async function initSchema(event) {
264
274
  }
265
275
  async function getSchemaVersion(client) {
266
276
  try {
267
- const result = await client.db.all(
268
- sql`SELECT version FROM _ai_ready_info WHERE id = 'schema'`
269
- );
270
- return result?.[0]?.version || null;
271
- } catch {
272
- return null;
277
+ const [row] = await client.db.select({ version: info.version }).from(info).where(eq(info.id, "schema")).limit(1);
278
+ return row?.version || null;
279
+ } catch (error) {
280
+ if (isMissingInfoTableError(error))
281
+ return null;
282
+ throw error;
273
283
  }
274
284
  }
275
285
  async function getStoredTokenizer(client) {
276
286
  try {
277
- const result = await client.db.all(
278
- sql`SELECT value FROM _ai_ready_info WHERE id = 'fts_tokenizer'`
279
- );
280
- return result?.[0]?.value || null;
281
- } catch {
282
- return null;
287
+ const [row] = await client.db.select({ value: info.value }).from(info).where(eq(info.id, "fts_tokenizer")).limit(1);
288
+ return row?.value || null;
289
+ } catch (error) {
290
+ if (isMissingInfoTableError(error))
291
+ return null;
292
+ throw error;
293
+ }
294
+ }
295
+ function isMissingInfoTableError(error) {
296
+ let current = error;
297
+ const seen = /* @__PURE__ */ new Set();
298
+ while (current && typeof current === "object" && !seen.has(current)) {
299
+ seen.add(current);
300
+ const candidate = current;
301
+ if (candidate.code === "42P01")
302
+ return true;
303
+ if (typeof candidate.message === "string" && candidate.message.includes("no such table") && candidate.message.includes("_ai_ready_info")) {
304
+ return true;
305
+ }
306
+ current = candidate.cause;
283
307
  }
308
+ return false;
284
309
  }
285
310
  const SQLITE_DROP_STATEMENTS = [
286
311
  sql`DROP TABLE IF EXISTS ai_ready_pages_fts`,
@@ -305,6 +330,19 @@ async function dropPostgresTables(client) {
305
330
  for (const stmt of POSTGRES_DROP_STATEMENTS)
306
331
  await client.db.execute(stmt);
307
332
  }
333
+ async function migratePostgresIntegerTimestamps(client) {
334
+ await getRawExecutor(client).batch([
335
+ { sql: "ALTER TABLE ai_ready_pages ALTER COLUMN indexed_at TYPE BIGINT USING indexed_at::BIGINT" },
336
+ { sql: "ALTER TABLE ai_ready_pages ALTER COLUMN last_seen_at TYPE BIGINT USING last_seen_at::BIGINT" },
337
+ { sql: "ALTER TABLE ai_ready_cron_runs ALTER COLUMN started_at TYPE BIGINT USING started_at::BIGINT" },
338
+ { sql: "ALTER TABLE ai_ready_cron_runs ALTER COLUMN finished_at TYPE BIGINT USING finished_at::BIGINT" },
339
+ { sql: "ALTER TABLE ai_ready_sitemaps ALTER COLUMN last_crawled_at TYPE BIGINT USING last_crawled_at::BIGINT" },
340
+ {
341
+ sql: "UPDATE _ai_ready_info SET version = ? WHERE id = 'schema'",
342
+ params: [POSTGRES_SCHEMA_VERSION]
343
+ }
344
+ ]);
345
+ }
308
346
  async function createSQLiteTables(client, ftsTokenizer) {
309
347
  const statements = [
310
348
  sql`CREATE TABLE IF NOT EXISTS ai_ready_pages (
@@ -404,11 +442,11 @@ async function createPostgresTables(client) {
404
442
  keywords TEXT NOT NULL DEFAULT '[]',
405
443
  content_hash TEXT,
406
444
  updated_at TEXT NOT NULL,
407
- indexed_at INTEGER NOT NULL,
445
+ indexed_at BIGINT NOT NULL,
408
446
  is_error INTEGER NOT NULL DEFAULT 0,
409
447
  indexed INTEGER NOT NULL DEFAULT 0,
410
448
  source TEXT NOT NULL DEFAULT 'prerender',
411
- last_seen_at INTEGER,
449
+ last_seen_at BIGINT,
412
450
  locale TEXT NOT NULL DEFAULT ''
413
451
  )`,
414
452
  sql`CREATE TABLE IF NOT EXISTS _ai_ready_info (
@@ -420,8 +458,8 @@ async function createPostgresTables(client) {
420
458
  )`,
421
459
  sql`CREATE TABLE IF NOT EXISTS ai_ready_cron_runs (
422
460
  id SERIAL PRIMARY KEY,
423
- started_at INTEGER NOT NULL,
424
- finished_at INTEGER,
461
+ started_at BIGINT NOT NULL,
462
+ finished_at BIGINT,
425
463
  duration_ms INTEGER,
426
464
  pages_indexed INTEGER DEFAULT 0,
427
465
  pages_remaining INTEGER DEFAULT 0,
@@ -431,7 +469,7 @@ async function createPostgresTables(client) {
431
469
  sql`CREATE TABLE IF NOT EXISTS ai_ready_sitemaps (
432
470
  name TEXT PRIMARY KEY,
433
471
  route TEXT NOT NULL,
434
- last_crawled_at INTEGER,
472
+ last_crawled_at BIGINT,
435
473
  url_count INTEGER DEFAULT 0,
436
474
  error_count INTEGER DEFAULT 0,
437
475
  last_error TEXT,
@@ -640,21 +678,24 @@ export async function seedRoutes(event, routes) {
640
678
  const route = normalizeRoute(raw);
641
679
  byRoute.set(route, normalizeRouteKey(route));
642
680
  }
643
- const ROWS_PER_INSERT = 20;
681
+ const rowsPerInsert = maxRowsPerInsert(4);
644
682
  const db = await useRawDb(event);
645
683
  const now = (/* @__PURE__ */ new Date()).toISOString();
646
684
  const nowMs = Date.now();
647
685
  const entries = [...byRoute.entries()];
648
686
  const stmts = [];
649
- for (let i = 0; i < entries.length; i += ROWS_PER_INSERT) {
650
- const batch = entries.slice(i, i + ROWS_PER_INSERT);
687
+ for (let i = 0; i < entries.length; i += rowsPerInsert) {
688
+ const batch = entries.slice(i, i + rowsPerInsert);
651
689
  const valuesSql = batch.map(() => `(?, ?, '', '', '', '[]', '[]', ?, 0, 0, 0, 'runtime', ?)`).join(", ");
652
690
  const params = batch.flatMap(([route, routeKey]) => [route, routeKey, now, nowMs]);
653
691
  stmts.push({
654
692
  sql: `
655
693
  INSERT INTO ai_ready_pages (route, route_key, title, description, markdown, headings, keywords, updated_at, indexed_at, is_error, indexed, source, last_seen_at)
656
694
  VALUES ${valuesSql}
657
- ON CONFLICT(route) DO UPDATE SET last_seen_at = excluded.last_seen_at
695
+ ON CONFLICT(route) DO UPDATE SET
696
+ last_seen_at = excluded.last_seen_at,
697
+ is_error = 0,
698
+ indexed = CASE WHEN ai_ready_pages.is_error = 1 THEN 0 ELSE ai_ready_pages.indexed END
658
699
  `,
659
700
  params
660
701
  });
@@ -7,7 +7,7 @@ import type { DrizzleDatabase } from './client.js';
7
7
  /**
8
8
  * Register the underlying driver for raw SQL access
9
9
  */
10
- export declare function registerDriver(db: DrizzleDatabase['db'], type: 'better-sqlite3' | 'node-sqlite' | 'libsql' | 'neon' | 'd1', driver: unknown): void;
10
+ export declare function registerDriver(db: DrizzleDatabase['db'], type: 'better-sqlite3' | 'node-sqlite' | 'libsql' | 'neon' | 'postgres' | 'd1', driver: unknown): void;
11
11
  /**
12
12
  * Get raw SQL executor for a Drizzle client
13
13
  */
@@ -33,4 +33,4 @@ export declare function useRawDb(event?: H3Event): Promise<RawExecutor>;
33
33
  /**
34
34
  * Close underlying database driver connection
35
35
  */
36
- export declare function closeDriver(db: DrizzleDatabase['db']): void;
36
+ export declare function closeDriver(db: DrizzleDatabase['db']): Promise<void>;
@@ -5,6 +5,10 @@ export function registerDriver(db, type, driver) {
5
5
  }
6
6
  const RE_PARAM_PLACEHOLDER = /\?/g;
7
7
  const MAX_BATCH_STATEMENTS = 100;
8
+ function toPostgresQuery(query) {
9
+ let index = 0;
10
+ return query.replace(RE_PARAM_PLACEHOLDER, () => `$${++index}`);
11
+ }
8
12
  export function getRawExecutor(client) {
9
13
  const cached = driverCache.get(client.db);
10
14
  if (!cached) {
@@ -35,11 +39,13 @@ export function getRawExecutor(client) {
35
39
  }
36
40
  case "neon": {
37
41
  const sqlFn = driver;
38
- let idx = 0;
39
- const pgQuery = query.replace(RE_PARAM_PLACEHOLDER, () => `$${++idx}`);
40
- const result = await sqlFn.query(pgQuery, params);
42
+ const result = await sqlFn.query(toPostgresQuery(query), params);
41
43
  return result.rows || result;
42
44
  }
45
+ case "postgres": {
46
+ const sqlClient = driver;
47
+ return await sqlClient.unsafe(toPostgresQuery(query), params);
48
+ }
43
49
  }
44
50
  },
45
51
  async first(query, params = []) {
@@ -70,9 +76,12 @@ export function getRawExecutor(client) {
70
76
  }
71
77
  case "neon": {
72
78
  const sqlFn = driver;
73
- let idx = 0;
74
- const pgQuery = query.replace(RE_PARAM_PLACEHOLDER, () => `$${++idx}`);
75
- await sqlFn.query(pgQuery, params);
79
+ await sqlFn.query(toPostgresQuery(query), params);
80
+ break;
81
+ }
82
+ case "postgres": {
83
+ const sqlClient = driver;
84
+ await sqlClient.unsafe(toPostgresQuery(query), params);
76
85
  break;
77
86
  }
78
87
  }
@@ -127,12 +136,17 @@ export function getRawExecutor(client) {
127
136
  const sqlFn = driver;
128
137
  for (let i = 0; i < queries.length; i += MAX_BATCH_STATEMENTS) {
129
138
  const chunk = queries.slice(i, i + MAX_BATCH_STATEMENTS);
130
- const pgQueries = chunk.map((q) => {
131
- let idx = 0;
132
- const pgQuery = q.sql.replace(RE_PARAM_PLACEHOLDER, () => `$${++idx}`);
133
- return sqlFn.query(pgQuery, q.params || []);
134
- });
135
- await sqlFn.transaction(pgQueries);
139
+ await sqlFn.transaction((tx) => chunk.map((q) => tx.query(toPostgresQuery(q.sql), q.params || [])));
140
+ }
141
+ break;
142
+ }
143
+ case "postgres": {
144
+ const sqlClient = driver;
145
+ for (let i = 0; i < queries.length; i += MAX_BATCH_STATEMENTS) {
146
+ const chunk = queries.slice(i, i + MAX_BATCH_STATEMENTS);
147
+ await sqlClient.begin((transaction) => Promise.all(chunk.map(
148
+ (q) => transaction.unsafe(toPostgresQuery(q.sql), q.params || [])
149
+ )));
136
150
  }
137
151
  break;
138
152
  }
@@ -144,7 +158,7 @@ export async function useRawDb(event) {
144
158
  const client = await useDrizzle(event);
145
159
  return getRawExecutor(client);
146
160
  }
147
- export function closeDriver(db) {
161
+ export async function closeDriver(db) {
148
162
  const cached = driverCache.get(db);
149
163
  if (!cached)
150
164
  return;
@@ -165,6 +179,11 @@ export function closeDriver(db) {
165
179
  client.close?.();
166
180
  break;
167
181
  }
182
+ case "postgres": {
183
+ const sqlClient = driver;
184
+ await sqlClient.end();
185
+ break;
186
+ }
168
187
  }
169
188
  driverCache.delete(db);
170
189
  }
@@ -48,6 +48,7 @@ export interface QueryPagesOptions {
48
48
  limit?: number;
49
49
  offset?: number;
50
50
  }
51
+ type DatabaseNumber = number | string | bigint;
51
52
  /**
52
53
  * Get lastmod (updatedAt) for all indexed pages
53
54
  * Returns a Map for O(1) lookup when enriching sitemaps
@@ -92,7 +93,7 @@ export interface SearchPagesOptions {
92
93
  limit?: number;
93
94
  }
94
95
  /**
95
- * Full-text search using FTS5
96
+ * Full-text search using FTS5 or PostgreSQL ILIKE
96
97
  * Note: FTS is only available at runtime, not during prerender
97
98
  */
98
99
  export declare function searchPages(event: H3Event | undefined, query: string, options?: SearchPagesOptions): Promise<SearchResult[]>;
@@ -139,15 +140,8 @@ export declare function getPageHash(event: H3Event | undefined, route: string):
139
140
  export declare function seedRoutes(event: H3Event | undefined, routes: Array<string | {
140
141
  route: string;
141
142
  locale?: string;
143
+ url?: string;
142
144
  }>): Promise<number>;
143
- /**
144
- * Get sitemap seeded timestamp from _ai_ready_info
145
- */
146
- export declare function getSitemapSeededAt(event: H3Event | undefined): Promise<number | undefined>;
147
- /**
148
- * Set sitemap seeded timestamp
149
- */
150
- export declare function setSitemapSeededAt(event: H3Event | undefined, timestamp: number): Promise<void>;
151
145
  /**
152
146
  * Prune routes not seen in sitemap for longer than threshold
153
147
  * Only prunes routes with source='runtime' (never prerendered pages)
@@ -159,11 +153,11 @@ export declare function pruneStaleRoutes(event: H3Event | undefined, staleThresh
159
153
  export declare function getStaleRoutes(event: H3Event | undefined, staleThresholdSeconds: number): Promise<string[]>;
160
154
  export interface CronRunRow {
161
155
  id: number;
162
- started_at: number;
163
- finished_at: number | null;
164
- duration_ms: number | null;
165
- pages_indexed: number;
166
- pages_remaining: number;
156
+ started_at: DatabaseNumber;
157
+ finished_at: DatabaseNumber | null;
158
+ duration_ms: DatabaseNumber | null;
159
+ pages_indexed: DatabaseNumber;
160
+ pages_remaining: DatabaseNumber;
167
161
  errors: string;
168
162
  status: 'running' | 'success' | 'partial' | 'error';
169
163
  }
@@ -193,10 +187,6 @@ export declare function completeCronRun(event: H3Event | undefined, runId: numbe
193
187
  * Get recent cron runs
194
188
  */
195
189
  export declare function getRecentCronRuns(event: H3Event | undefined, limit?: number): Promise<CronRun[]>;
196
- /**
197
- * Clean up old cron runs (keep last N)
198
- */
199
- export declare function cleanupOldCronRuns(event: H3Event | undefined, keepCount?: number): Promise<number>;
200
190
  /**
201
191
  * Clean up cron runs older than specified age
202
192
  */
@@ -213,15 +203,27 @@ export interface CronFastPathStatus {
213
203
  * Reduces 6+ sequential DB calls to 1
214
204
  */
215
205
  export declare function getCronFastPathStatus(event: H3Event | undefined, sitemapIntervalMinutes?: number): Promise<CronFastPathStatus | null>;
216
- /**
217
- * Try to acquire cron lock. Returns true if acquired, false if another run is active.
218
- * Uses atomic INSERT OR REPLACE with conditional check to prevent race conditions.
219
- */
220
- export declare function tryAcquireCronLock(event: H3Event | undefined): Promise<boolean>;
221
- /**
222
- * Release cron lock
223
- */
224
- export declare function releaseCronLock(event: H3Event | undefined): Promise<void>;
206
+ export type CronLockAcquire = {
207
+ _tag: 'acquired';
208
+ token: string;
209
+ } | {
210
+ _tag: 'held';
211
+ };
212
+ /**
213
+ * Try to acquire cron lock. Returns the ownership token on success, `held`
214
+ * when a live lock belongs to another run.
215
+ *
216
+ * The value carries a random token plus expiry in one row, so the
217
+ * compare-expired-then-set upsert is atomic and ownership is unique even for
218
+ * two isolates that start in the same millisecond. A legacy value that
219
+ * predates tokens parses as no expiry, so it is treated as expired.
220
+ */
221
+ export declare function tryAcquireCronLock(event: H3Event | undefined): Promise<CronLockAcquire>;
222
+ /**
223
+ * Release cron lock. Only deletes the row when its token is ours, so a run
224
+ * that outlived the lock TTL cannot delete the new owner's lock.
225
+ */
226
+ export declare function releaseCronLock(event: H3Event | undefined, token: string): Promise<void>;
225
227
  export interface CronLockStatus {
226
228
  held: boolean;
227
229
  since: number | null;
@@ -303,3 +305,4 @@ export declare function getRecentlyIndexedPages(event: H3Event | undefined, limi
303
305
  * Count pages indexed in a time window
304
306
  */
305
307
  export declare function countRecentlyIndexed(event: H3Event | undefined, sinceMs: number): Promise<number>;
308
+ export {};