@syncular/server-hono 0.0.6-95 → 0.0.6-96

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.
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/console/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAQ9E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAqE5B,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EAEnB,0BAA0B,EAC3B,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,mBAAmB,CA0DtB;AAmOD,wBAAgB,mBAAmB,CACjC,EAAE,SAAS,UAAU,EACrB,IAAI,SAAS,cAAc,EAC3B,CAAC,SAAS,SAAS,GAAG,SAAS,EAC/B,OAAO,EAAE,0BAA0B,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAmuGxD;AA6BD;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,CAAC,EAAE,MAAM,GACb,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAgBnD"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/console/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAQ9E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAqE5B,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EAEnB,0BAA0B,EAC3B,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,mBAAmB,CA0DtB;AAsPD,wBAAgB,mBAAmB,CACjC,EAAE,SAAS,UAAU,EACrB,IAAI,SAAS,cAAc,EAC3B,CAAC,SAAS,SAAS,GAAG,SAAS,EAC/B,OAAO,EAAE,0BAA0B,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CA44GxD;AA6BD;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,CAAC,EAAE,MAAM,GACb,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAgBnD"}
@@ -262,6 +262,20 @@ const apiKeysQuerySchema = ConsolePaginationQuerySchema.extend({
262
262
  const handlersResponseSchema = z.object({
263
263
  items: z.array(ConsoleHandlerSchema),
264
264
  });
265
+ const DEFAULT_REQUEST_EVENTS_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
266
+ const DEFAULT_REQUEST_EVENTS_MAX_ROWS = 10_000;
267
+ const DEFAULT_OPERATION_EVENTS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
268
+ const DEFAULT_OPERATION_EVENTS_MAX_ROWS = 5_000;
269
+ const DEFAULT_AUTO_EVENTS_PRUNE_INTERVAL_MS = 5 * 60 * 1000;
270
+ function readNonNegativeInteger(value, fallback) {
271
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
272
+ return fallback;
273
+ }
274
+ if (value < 0) {
275
+ return fallback;
276
+ }
277
+ return Math.floor(value);
278
+ }
265
279
  export function createConsoleRoutes(options) {
266
280
  const routes = new Hono();
267
281
  routes.onError((error, context) => {
@@ -275,6 +289,12 @@ export function createConsoleRoutes(options) {
275
289
  const db = options.db;
276
290
  const metricsAggregationMode = options.metrics?.aggregationMode ?? 'auto';
277
291
  const rawFallbackMaxEvents = Math.max(1, options.metrics?.rawFallbackMaxEvents ?? 5000);
292
+ const requestEventsMaxAgeMs = readNonNegativeInteger(options.maintenance?.requestEventsMaxAgeMs, DEFAULT_REQUEST_EVENTS_MAX_AGE_MS);
293
+ const requestEventsMaxRows = readNonNegativeInteger(options.maintenance?.requestEventsMaxRows, DEFAULT_REQUEST_EVENTS_MAX_ROWS);
294
+ const operationEventsMaxAgeMs = readNonNegativeInteger(options.maintenance?.operationEventsMaxAgeMs, DEFAULT_OPERATION_EVENTS_MAX_AGE_MS);
295
+ const operationEventsMaxRows = readNonNegativeInteger(options.maintenance?.operationEventsMaxRows, DEFAULT_OPERATION_EVENTS_MAX_ROWS);
296
+ const autoEventsPruneIntervalMs = readNonNegativeInteger(options.maintenance?.autoPruneIntervalMs, DEFAULT_AUTO_EVENTS_PRUNE_INTERVAL_MS);
297
+ let lastEventsPruneRunAt = 0;
278
298
  // Ensure console schema exists before handlers query console tables.
279
299
  const consoleSchemaReadyPromise = (options.consoleSchemaReady ??
280
300
  options.dialect.ensureConsoleSchema?.(options.db) ??
@@ -319,6 +339,12 @@ export function createConsoleRoutes(options) {
319
339
  }
320
340
  await next();
321
341
  });
342
+ routes.use('*', async (c, next) => {
343
+ if (c.req.method !== 'OPTIONS') {
344
+ triggerAutomaticEventsPrune();
345
+ }
346
+ await next();
347
+ });
322
348
  // Auth middleware
323
349
  const requireAuth = async (c) => {
324
350
  const auth = await options.authenticate(c);
@@ -414,6 +440,151 @@ export function createConsoleRoutes(options) {
414
440
  .executeTakeFirst();
415
441
  return Number(result?.numDeletedRows ?? 0);
416
442
  };
443
+ const pruneRequestEventsByAge = async () => {
444
+ if (requestEventsMaxAgeMs <= 0) {
445
+ return 0;
446
+ }
447
+ const cutoffDate = new Date(Date.now() - requestEventsMaxAgeMs);
448
+ const result = await db
449
+ .deleteFrom('sync_request_events')
450
+ .where('created_at', '<', cutoffDate.toISOString())
451
+ .executeTakeFirst();
452
+ return Number(result?.numDeletedRows ?? 0);
453
+ };
454
+ const pruneRequestEventsByCount = async () => {
455
+ if (requestEventsMaxRows <= 0) {
456
+ return 0;
457
+ }
458
+ const countRow = await db
459
+ .selectFrom('sync_request_events')
460
+ .select(({ fn }) => fn.countAll().as('total'))
461
+ .executeTakeFirst();
462
+ const total = coerceNumber(countRow?.total) ?? 0;
463
+ if (total <= requestEventsMaxRows) {
464
+ return 0;
465
+ }
466
+ const cutoffRow = await db
467
+ .selectFrom('sync_request_events')
468
+ .select(['event_id'])
469
+ .orderBy('event_id', 'desc')
470
+ .offset(requestEventsMaxRows)
471
+ .limit(1)
472
+ .executeTakeFirst();
473
+ const cutoffEventId = coerceNumber(cutoffRow?.event_id);
474
+ if (cutoffEventId === null) {
475
+ return 0;
476
+ }
477
+ const result = await db
478
+ .deleteFrom('sync_request_events')
479
+ .where('event_id', '<=', cutoffEventId)
480
+ .executeTakeFirst();
481
+ return Number(result?.numDeletedRows ?? 0);
482
+ };
483
+ const pruneOperationEventsByAge = async () => {
484
+ if (operationEventsMaxAgeMs <= 0) {
485
+ return 0;
486
+ }
487
+ const cutoffDate = new Date(Date.now() - operationEventsMaxAgeMs);
488
+ const result = await db
489
+ .deleteFrom('sync_operation_events')
490
+ .where('created_at', '<', cutoffDate.toISOString())
491
+ .executeTakeFirst();
492
+ return Number(result?.numDeletedRows ?? 0);
493
+ };
494
+ const pruneOperationEventsByCount = async () => {
495
+ if (operationEventsMaxRows <= 0) {
496
+ return 0;
497
+ }
498
+ const countRow = await db
499
+ .selectFrom('sync_operation_events')
500
+ .select(({ fn }) => fn.countAll().as('total'))
501
+ .executeTakeFirst();
502
+ const total = coerceNumber(countRow?.total) ?? 0;
503
+ if (total <= operationEventsMaxRows) {
504
+ return 0;
505
+ }
506
+ const cutoffRow = await db
507
+ .selectFrom('sync_operation_events')
508
+ .select(['operation_id'])
509
+ .orderBy('operation_id', 'desc')
510
+ .offset(operationEventsMaxRows)
511
+ .limit(1)
512
+ .executeTakeFirst();
513
+ const cutoffOperationId = coerceNumber(cutoffRow?.operation_id);
514
+ if (cutoffOperationId === null) {
515
+ return 0;
516
+ }
517
+ const result = await db
518
+ .deleteFrom('sync_operation_events')
519
+ .where('operation_id', '<=', cutoffOperationId)
520
+ .executeTakeFirst();
521
+ return Number(result?.numDeletedRows ?? 0);
522
+ };
523
+ const pruneConsoleEvents = async () => {
524
+ const requestEventsDeletedByAge = await pruneRequestEventsByAge();
525
+ const requestEventsDeletedByCount = await pruneRequestEventsByCount();
526
+ const requestEventsDeleted = requestEventsDeletedByAge + requestEventsDeletedByCount;
527
+ const operationEventsDeletedByAge = await pruneOperationEventsByAge();
528
+ const operationEventsDeletedByCount = await pruneOperationEventsByCount();
529
+ const operationEventsDeleted = operationEventsDeletedByAge + operationEventsDeletedByCount;
530
+ const payloadSnapshotsDeleted = await deleteUnreferencedPayloadSnapshots();
531
+ const totalDeleted = requestEventsDeleted + operationEventsDeleted;
532
+ return {
533
+ requestEventsDeleted,
534
+ operationEventsDeleted,
535
+ payloadSnapshotsDeleted,
536
+ totalDeleted,
537
+ };
538
+ };
539
+ let eventsPrunePromise = null;
540
+ const runEventsPrune = async () => {
541
+ if (eventsPrunePromise) {
542
+ return eventsPrunePromise;
543
+ }
544
+ let pending;
545
+ pending = pruneConsoleEvents()
546
+ .then((result) => {
547
+ lastEventsPruneRunAt = Date.now();
548
+ return result;
549
+ })
550
+ .finally(() => {
551
+ if (eventsPrunePromise === pending) {
552
+ eventsPrunePromise = null;
553
+ }
554
+ });
555
+ eventsPrunePromise = pending;
556
+ return pending;
557
+ };
558
+ const triggerAutomaticEventsPrune = () => {
559
+ if (autoEventsPruneIntervalMs <= 0) {
560
+ return;
561
+ }
562
+ if (eventsPrunePromise) {
563
+ return;
564
+ }
565
+ if (Date.now() - lastEventsPruneRunAt < autoEventsPruneIntervalMs) {
566
+ return;
567
+ }
568
+ void runEventsPrune()
569
+ .then((result) => {
570
+ if (result.totalDeleted <= 0 && result.payloadSnapshotsDeleted <= 0) {
571
+ return;
572
+ }
573
+ logSyncEvent({
574
+ event: 'console.prune_events_auto',
575
+ deletedCount: result.totalDeleted,
576
+ requestEventsDeleted: result.requestEventsDeleted,
577
+ operationEventsDeleted: result.operationEventsDeleted,
578
+ payloadDeletedCount: result.payloadSnapshotsDeleted,
579
+ });
580
+ })
581
+ .catch((error) => {
582
+ logSyncEvent({
583
+ event: 'console.prune_events_auto_failed',
584
+ error: error instanceof Error ? error.message : String(error),
585
+ });
586
+ });
587
+ };
417
588
  const recordOperationEvent = async (event) => {
418
589
  await db
419
590
  .insertInto('sync_operation_events')
@@ -2095,47 +2266,15 @@ export function createConsoleRoutes(options) {
2095
2266
  const auth = await requireAuth(c);
2096
2267
  if (!auth)
2097
2268
  return c.json({ error: 'UNAUTHENTICATED' }, 401);
2098
- // Prune events older than 7 days or keep max 10000 events
2099
- const cutoffDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
2100
- // Delete by date first
2101
- const resByDate = await db
2102
- .deleteFrom('sync_request_events')
2103
- .where('created_at', '<', cutoffDate.toISOString())
2104
- .executeTakeFirst();
2105
- let deletedCount = Number(resByDate?.numDeletedRows ?? 0);
2106
- // Then delete oldest if we still have more than 10000 events
2107
- const countRow = await db
2108
- .selectFrom('sync_request_events')
2109
- .select(({ fn }) => fn.countAll().as('total'))
2110
- .executeTakeFirst();
2111
- const total = coerceNumber(countRow?.total) ?? 0;
2112
- const maxEvents = 10000;
2113
- if (total > maxEvents) {
2114
- // Find event_id cutoff to keep only newest maxEvents
2115
- const cutoffRow = await db
2116
- .selectFrom('sync_request_events')
2117
- .select(['event_id'])
2118
- .orderBy('event_id', 'desc')
2119
- .offset(maxEvents)
2120
- .limit(1)
2121
- .executeTakeFirst();
2122
- if (cutoffRow) {
2123
- const cutoffEventId = coerceNumber(cutoffRow.event_id);
2124
- if (cutoffEventId !== null) {
2125
- const resByCount = await db
2126
- .deleteFrom('sync_request_events')
2127
- .where('event_id', '<=', cutoffEventId)
2128
- .executeTakeFirst();
2129
- deletedCount += Number(resByCount?.numDeletedRows ?? 0);
2130
- }
2131
- }
2132
- }
2133
- const payloadDeletedCount = await deleteUnreferencedPayloadSnapshots();
2269
+ const pruneResult = await runEventsPrune();
2270
+ const deletedCount = pruneResult.totalDeleted;
2134
2271
  logSyncEvent({
2135
2272
  event: 'console.prune_events',
2136
2273
  consoleUserId: auth.consoleUserId,
2137
2274
  deletedCount,
2138
- payloadDeletedCount,
2275
+ requestEventsDeleted: pruneResult.requestEventsDeleted,
2276
+ operationEventsDeleted: pruneResult.operationEventsDeleted,
2277
+ payloadDeletedCount: pruneResult.payloadSnapshotsDeleted,
2139
2278
  });
2140
2279
  const result = { deletedCount };
2141
2280
  return c.json(result, 200);