feat(sync): add deterministic maintenance-thread dedup key
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Pure, deterministic dedup key for maintenance threads.
|
||||||
|
*
|
||||||
|
* Derives a stable key from the merge-base + head SHAs so that repeated
|
||||||
|
* scheduled sync runs against the same upstream state collapse onto a single
|
||||||
|
* maintenance thread instead of creating a new one each run. NEVER involves
|
||||||
|
* Date.now()/random — the key must be identical for identical inputs.
|
||||||
|
*
|
||||||
|
* Returns `null` when the head SHA is missing/empty (unresolvable head → the
|
||||||
|
* caller cannot dedup and should SKIP thread creation rather than mint a
|
||||||
|
* unique key).
|
||||||
|
*/
|
||||||
|
export const maintenanceDedupKey = (input: {
|
||||||
|
mergeBaseSha?: string;
|
||||||
|
headSha?: string;
|
||||||
|
}): string | null => {
|
||||||
|
const head = input.headSha?.trim();
|
||||||
|
if (!head) return null;
|
||||||
|
const base = input.mergeBaseSha?.trim() || 'nobase';
|
||||||
|
return `${base}:${head}`;
|
||||||
|
};
|
||||||
@@ -797,6 +797,7 @@ const applicationTables = {
|
|||||||
),
|
),
|
||||||
ignoredCommitShas: v.optional(v.array(v.string())),
|
ignoredCommitShas: v.optional(v.array(v.string())),
|
||||||
ignoredReason: v.optional(v.string()),
|
ignoredReason: v.optional(v.string()),
|
||||||
|
dedupKey: v.optional(v.string()),
|
||||||
createdAt: v.number(),
|
createdAt: v.number(),
|
||||||
updatedAt: v.number(),
|
updatedAt: v.number(),
|
||||||
resolvedAt: v.optional(v.number()),
|
resolvedAt: v.optional(v.number()),
|
||||||
@@ -805,7 +806,8 @@ const applicationTables = {
|
|||||||
.index('by_owner_status', ['ownerId', 'status'])
|
.index('by_owner_status', ['ownerId', 'status'])
|
||||||
.index('by_spoon', ['spoonId'])
|
.index('by_spoon', ['spoonId'])
|
||||||
.index('by_source', ['ownerId', 'source'])
|
.index('by_source', ['ownerId', 'source'])
|
||||||
.index('by_created', ['createdAt']),
|
.index('by_created', ['createdAt'])
|
||||||
|
.index('by_spoon_dedup', ['spoonId', 'dedupKey']),
|
||||||
threadMessages: defineTable({
|
threadMessages: defineTable({
|
||||||
threadId: v.id('threads'),
|
threadId: v.id('threads'),
|
||||||
ownerId: v.id('users'),
|
ownerId: v.id('users'),
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import { maintenanceDedupKey } from '../../convex/maintenanceDedup';
|
||||||
|
|
||||||
|
describe('maintenanceDedupKey', () => {
|
||||||
|
test('combines mergeBase and head into a stable key', () => {
|
||||||
|
expect(maintenanceDedupKey({ mergeBaseSha: 'a', headSha: 'b' })).toBe('a:b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to nobase when mergeBase is missing', () => {
|
||||||
|
expect(maintenanceDedupKey({ headSha: 'b' })).toBe('nobase:b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when head is missing', () => {
|
||||||
|
expect(maintenanceDedupKey({ mergeBaseSha: 'a' })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when head is empty', () => {
|
||||||
|
expect(maintenanceDedupKey({ mergeBaseSha: 'a', headSha: '' })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when head is only whitespace', () => {
|
||||||
|
expect(maintenanceDedupKey({ mergeBaseSha: 'a', headSha: ' ' })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('is deterministic across repeated calls (no Date.now/random)', () => {
|
||||||
|
const input = { mergeBaseSha: 'base123', headSha: 'head456' };
|
||||||
|
const first = maintenanceDedupKey(input);
|
||||||
|
const second = maintenanceDedupKey(input);
|
||||||
|
expect(first).toBe(second);
|
||||||
|
expect(first).toBe('base123:head456');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('different heads produce different keys', () => {
|
||||||
|
const a = maintenanceDedupKey({ mergeBaseSha: 'base', headSha: 'h1' });
|
||||||
|
const b = maintenanceDedupKey({ mergeBaseSha: 'base', headSha: 'h2' });
|
||||||
|
expect(a).not.toBe(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trims surrounding whitespace on inputs', () => {
|
||||||
|
expect(
|
||||||
|
maintenanceDedupKey({ mergeBaseSha: ' a ', headSha: ' b ' }),
|
||||||
|
).toBe('a:b');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user