diff --git a/.changeset/lean-contrail.md b/.changeset/lean-contrail.md index 2015b36..b11067c 100644 --- a/.changeset/lean-contrail.md +++ b/.changeset/lean-contrail.md @@ -2,4 +2,4 @@ "@atmo-dev/contrail": minor --- -Collapse Contrail into one public package and one AppView implementation. Remove the spaces, authority, record-host, community, realtime, sync, and custom Lexicon-tooling products. Route Jetstream, persistent, backfill, refresh, and immediate synchronization records through the shared `ingestRecords` admission and projection path. Make materialized relation counts converge when children arrive before parents or move during refresh, and prevent transient PDS failures from being interpreted as authoritative deletions. Keep dependent-subject filtering scoped to dependent collections and restore typed example XRPC clients with Atcute's generator. Preserve `node:sqlite` in the published adapter, make all query and search cursors stable across tied and typed/null rows, use Worker-safe cursor encoding, and bound the complete notify resolution/fetch/body operation. Admit newly discovered actors and their dependent mutations as one batch, keep subject decisions mutation-local so deletes always pass, and classify refresh statistics after admission. +Collapse Contrail into one public package and one AppView implementation. Remove the spaces, authority, record-host, community, realtime, sync, and custom Lexicon-tooling products. Route Jetstream, persistent, backfill, refresh, and immediate synchronization records through the shared `ingestRecords` admission and projection path. Make materialized relation counts converge when children arrive before parents or move during refresh, and prevent transient PDS failures from being interpreted as authoritative deletions. Keep dependent-subject filtering scoped to dependent collections and restore typed example XRPC clients with Atcute's generator. Preserve `node:sqlite` in the published adapter, make all query and search cursors stable across tied and typed/null rows, use Worker-safe cursor encoding, and bound the complete notify resolution/fetch/body operation. Admit newly discovered actors and their dependent mutations as one batch, keep subject decisions mutation-local so deletes always pass, and classify refresh statistics after admission. Existing pagination cursors from 0.12 are intentionally invalidated by the new stable cursor format; clients should discard persisted cursor tokens when upgrading. diff --git a/packages/contrail/src/core/labels/hydrate.ts b/packages/contrail/src/core/labels/hydrate.ts index 02b947a44ef764f5c31f64fefd468727f76fd7d1..1efd4d1ec0607b34f11f6670d126ad43c9942f55 100644 GIT binary patch delta 35 mcmcaD{Zx8`4X0R4seyrkfl75zez{(0QD!ZKzuALxG7A9RcMGuq delta 25 gcmaDVeOr2i4JQwSN_A0wxn5~eW-Y_!AkN7w0Cm*}j{pDw diff --git a/packages/contrail/src/core/router/notify.ts b/packages/contrail/src/core/router/notify.ts index 72782f6..a58ca2a 100644 --- a/packages/contrail/src/core/router/notify.ts +++ b/packages/contrail/src/core/router/notify.ts @@ -19,6 +19,7 @@ export function parseAtUri(uri: string): { did: string; collection: string; rkey } const NOTIFY_FETCH_TIMEOUT_MS = 5_000; +export const MAX_NOTIFY_URIS = 25; type RecordFetchResult = | { kind: "found"; value: Record; cid: string } @@ -131,6 +132,10 @@ export async function processNotifyUris( config: ContrailConfig, uris: string[] ): Promise { + if (uris.length > MAX_NOTIFY_URIS) { + throw new RangeError(`max ${MAX_NOTIFY_URIS} URIs per request`); + } + const events: IngestEvent[] = []; const errors: string[] = []; @@ -290,8 +295,8 @@ export function registerNotifyRoute( return c.json({ error: "uri or uris required" }, 400); } - if (uris.length > 25) { - return c.json({ error: "max 25 URIs per request" }, 400); + if (uris.length > MAX_NOTIFY_URIS) { + return c.json({ error: `max ${MAX_NOTIFY_URIS} URIs per request` }, 400); } const result = await processNotifyUris(db, config, uris); diff --git a/packages/contrail/src/index.ts b/packages/contrail/src/index.ts index a4b9c15..391d623 100644 --- a/packages/contrail/src/index.ts +++ b/packages/contrail/src/index.ts @@ -6,7 +6,13 @@ export type { AppOptions, ContrailOptions } from "./contrail"; export * from "./core/types"; export * from "./core/dialect"; export * from "./core/identity"; -export * from "./core/client"; +export { + getClient, + getPDS, + resolvePDS, + validateExternalUrl, +} from "./core/client"; +export type { ResolvedIdentity } from "./core/client"; export * from "./core/sinks/types"; // Ingestion and maintenance. diff --git a/packages/contrail/tests/client.test.ts b/packages/contrail/tests/client.test.ts index 7713f0a..021154b 100644 --- a/packages/contrail/tests/client.test.ts +++ b/packages/contrail/tests/client.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { resolvePDS, getClient, getPDS, __resetPdsCachesForTests } from "../src/index"; +import { resolvePDS, getClient, getPDS } from "../src/index"; +import { __resetPdsCachesForTests } from "../src/core/client"; import { type DidDocumentResolver } from "@atcute/identity-resolver"; import { createTestDbWithSchema } from "./helpers"; import type { Did } from "@atcute/lexicons"; diff --git a/packages/contrail/tests/get-record-handle.test.ts b/packages/contrail/tests/get-record-handle.test.ts index 8bbc977..8985834 100644 --- a/packages/contrail/tests/get-record-handle.test.ts +++ b/packages/contrail/tests/get-record-handle.test.ts @@ -5,7 +5,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { Contrail } from "../src/contrail"; import { createSqliteDatabase } from "../src/adapters/sqlite"; import { ingestRecords } from "../src/index"; -import { __resetPdsCachesForTests } from "../src/index"; +import { __resetPdsCachesForTests } from "../src/core/client"; import type { Database, IngestEvent } from "../src/index"; const COLL = "com.example.event"; diff --git a/packages/contrail/tests/identity-config.test.ts b/packages/contrail/tests/identity-config.test.ts index b47a932..9c3b70e 100644 --- a/packages/contrail/tests/identity-config.test.ts +++ b/packages/contrail/tests/identity-config.test.ts @@ -5,7 +5,7 @@ import { resolveActor, refreshStaleIdentities, } from "../src/index"; -import { __resetPdsCachesForTests } from "../src/index"; +import { __resetPdsCachesForTests } from "../src/core/client"; import { createTestDbWithSchema } from "./helpers"; import type { Did } from "@atcute/lexicons"; diff --git a/packages/contrail/tests/identity-handle.test.ts b/packages/contrail/tests/identity-handle.test.ts index 5483903..b5a735d 100644 --- a/packages/contrail/tests/identity-handle.test.ts +++ b/packages/contrail/tests/identity-handle.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { getPDS, __resetPdsCachesForTests } from "../src/index"; +import { getPDS } from "../src/index"; +import { __resetPdsCachesForTests } from "../src/core/client"; import { refreshStaleIdentities } from "../src/index"; import { createTestDbWithSchema } from "./helpers"; import type { Database } from "../src/index"; diff --git a/packages/contrail/tests/network-overrides-appview.test.ts b/packages/contrail/tests/network-overrides-appview.test.ts index 576c29e..31ee305 100644 --- a/packages/contrail/tests/network-overrides-appview.test.ts +++ b/packages/contrail/tests/network-overrides-appview.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { createApp } from "../src/index"; import { runIngestCycle } from "../src/index"; -import { __resetPdsCachesForTests } from "../src/index"; +import { __resetPdsCachesForTests } from "../src/core/client"; import { createTestDbWithSchema, TEST_CONFIG } from "./helpers"; import type { ContrailConfig } from "../src/index"; diff --git a/packages/contrail/tests/network-overrides.test.ts b/packages/contrail/tests/network-overrides.test.ts index 1f5af5d..cdff710 100644 --- a/packages/contrail/tests/network-overrides.test.ts +++ b/packages/contrail/tests/network-overrides.test.ts @@ -1,7 +1,8 @@ import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest"; import http from "node:http"; import type { AddressInfo } from "node:net"; -import { resolvePDS, getClient, __resetPdsCachesForTests } from "../src/index"; +import { resolvePDS, getClient } from "../src/index"; +import { __resetPdsCachesForTests } from "../src/core/client"; import { refreshStaleIdentities } from "../src/index"; import { createTestDbWithSchema } from "./helpers"; import type { ContrailConfig } from "../src/index"; diff --git a/packages/contrail/tests/notify.test.ts b/packages/contrail/tests/notify.test.ts index 7cbc5f4..75cee85 100644 --- a/packages/contrail/tests/notify.test.ts +++ b/packages/contrail/tests/notify.test.ts @@ -5,7 +5,7 @@ import { ingestRecords, createTestDb, createTestDbWithSchema, makeEvent, TEST_CO import { initSchema } from "../src/index"; import { parseAtUri } from "../src/index"; import { createApp } from "../src/index"; -import { queryRecords } from "../src/index"; +import { processNotifyUris, queryRecords } from "../src/index"; import type { Hono } from "hono"; const NOTIFY_CONFIG = { ...TEST_CONFIG, notify: true }; @@ -143,6 +143,15 @@ describe("POST notifyOfUpdate", () => { expect(body.error).toMatch(/max 25/); }); + it("enforces the batch limit for programmatic callers too", async () => { + const uris = Array.from({ length: 26 }, (_, index) => + `at://did:plc:test/community.lexicon.calendar.event/r${index}`, + ); + await expect( + processNotifyUris(db, NOTIFY_CONFIG, uris), + ).rejects.toThrow("max 25 URIs"); + }); + it("reports error for invalid AT URI", async () => { const res = await app.request(endpoint, { method: "POST",