Skip to content

service-messaging still carries its own hand-written isUniqueViolation() — migrate it onto the shared @objectstack/types predicate #6542

Description

@os-project-manager

Blocked-by: #6250 (the shared predicate must land first — PR #6541)

Filed by the #6250 developer agent per that issue's ruling: "OUT OF SCOPE — the two out-of-lane consumers. packages/services/service-messaging/** (domain:services) ... must NOT be edited. Migrating them onto the predicate is a follow-up per lane, filed once the predicate exists." The predicate now exists, so this is that filing.

What is left

packages/services/service-messaging/src/messaging-service.ts declares a private isUniqueViolation(err) — the most complete of the four copies #6250 inventoried, and the one isUniqueViolationError in @objectstack/types was seeded from:

function isUniqueViolation(err: unknown): boolean {
    const e = err as { code?: string | number; message?: string } | undefined;
    if (!e) return false;
    if (e.code === '23505' || e.code === 'ER_DUP_ENTRY' || e.code === 'SQLITE_CONSTRAINT_UNIQUE') return true;
    const msg = String(e.message ?? '').toLowerCase();
    return (
        msg.includes('unique constraint failed') ||
        msg.includes('duplicate key') ||
        msg.includes('duplicate entry')
    );
}

One call site: the lost check-then-act race on the inbox read-receipt unique index (isUniqueViolation(err) && (await flipToRead())).

Why this is finding and not a defect

No user hits anything today. This copy is three-dialect complete — it is the copy that was right. The cost is the fork itself: the next driver quirk gets taught to whichever copy its discoverer is standing in, and the two silently diverge. That is exactly how the REST copy came to miss MySQL while this one did not. Grade it in triage; do not read the finding label as "small".

The migration

@objectstack/service-messaging already depends on @objectstack/types, so this is an import swap plus deleting the local function. The shared predicate is a strict superset of this one on every channel:

That last point is the only behaviour change, and it is in the direction the call site wants (a wrapped conflict is still a conflict). Worth a test at the call site rather than a blind swap.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions