Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ layer("BitbucketPullRequestApi.layer", (it) => {
});

expect(filterOfCall(0)).toBe(
'(title ~ "page" OR description ~ "page") AND updated_on <= 2026-07-02T00:00:00+00:00',
'(title ~ "page" OR description ~ "page") AND updated_on < 2026-07-02T00:00:00.001Z',
);
}),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as Cache from "effect/Cache";
import * as Context from "effect/Context";
import * as DateTime from "effect/DateTime";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Result from "effect/Result";
import * as Schema from "effect/Schema";
import type {
Expand Down Expand Up @@ -326,6 +328,15 @@ function filterLiteral(value: string): string {
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
}

// Bitbucket keeps microseconds; include the cursor's whole millisecond before excluding seen IDs.
function updatedBeforeFilter(updatedBefore: string): string {
return Option.match(DateTime.make(updatedBefore), {
onNone: () => `updated_on <= ${updatedBefore}`,
onSome: (instant) =>
`updated_on < ${DateTime.formatIso(DateTime.add(instant, { milliseconds: 1 }))}`,
});
}

function mergeStrategy(method: PullRequestMergeMethod | undefined): string {
switch (method) {
case "squash":
Expand Down Expand Up @@ -544,12 +555,12 @@ export const make = Effect.gen(function* () {
listPullRequests: (input) =>
withRepository(input.repository, (path) => {
const search = input.query?.trim() ?? "";
// Exclude already-sent IDs at the inclusive timestamp before applying the page limit.
// Exclude already-sent IDs in the boundary millisecond before applying the page limit.
const predicates = [
...(search.length === 0 ? [] : [searchFilter(search)]),
...(input.author === undefined ? [] : [authorFilter(input.author)]),
...(input.draft === undefined ? [] : [`draft = ${input.draft}`]),
...(input.cursor === undefined ? [] : [`updated_on <= ${input.cursor.updatedBefore}`]),
...(input.cursor === undefined ? [] : [updatedBeforeFilter(input.cursor.updatedBefore)]),
...(input.cursor?.seenAt ?? []).map((number) => `id != ${number}`),
];
return listPage({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ import * as BitbucketPullRequestProvider from "./BitbucketPullRequestProvider.ts

it.effect("pages through rows that share an update instant by excluding those already sent", () =>
Effect.gen(function* () {
const boundary = "2026-10-12T00:00:00.000Z";
const older = "2026-10-11T00:00:00.000Z";
// Bitbucket stamps to the microsecond; 3 shares 1 and 2's millisecond but sorts after them.
const boundary = "2026-10-12T00:00:00.000500+00:00";
const sameMillisecond = "2026-10-12T00:00:00.000200+00:00";
const older = "2026-10-11T00:00:00.000000+00:00";
const micros = (stamp: string) =>
Date.parse(stamp) * 1000 + Number(/\.\d{3}(\d{3})/.exec(stamp)?.[1] ?? 0);
const rows = [1, 2, 3, 4, 5].map((id) => ({
id,
title: `Pull request ${id}`,
state: "OPEN",
created_on: older,
updated_on: id <= 3 ? boundary : older,
updated_on: id <= 2 ? boundary : id === 3 ? sameMillisecond : older,
source: { branch: { name: `feat/${id}` } },
destination: { branch: { name: "main" } },
links: { html: { href: `https://bitbucket.org/acme/web/pull-requests/${id}` } },
Expand All @@ -30,13 +34,16 @@ it.effect("pages through rows that share an update instant by excluding those al
const filter =
new URL(url, "https://api.bitbucket.org").searchParams.get("q") ?? "";
filters.push(filter);
const updatedBefore = /updated_on <= (\S+)/.exec(filter)?.[1];
const [, operator, bound] = /updated_on (<=?) (\S+)/.exec(filter) ?? [];
const excluded = new Set(
Array.from(filter.matchAll(/id != (\d+)/g), (match) => Number(match[1])),
);
const values = rows.filter(
(row) =>
(updatedBefore === undefined || row.updated_on <= updatedBefore) &&
(bound === undefined ||
(operator === "<"
? micros(row.updated_on) < micros(bound)
: micros(row.updated_on) <= micros(bound))) &&
!excluded.has(row.id),
);
return Effect.succeed({ body: JSON.stringify({ values }), truncated: false });
Expand Down Expand Up @@ -69,16 +76,25 @@ it.effect("pages through rows that share an update instant by excluding those al
);
assert.isTrue(first.truncated);

const second = yield* list({ updatedBefore: boundary, delivered: 2, seenAt: [1, 2] });
// The cursor carries the instant cut to the millisecond, as the app normalizes it.
const second = yield* list({
updatedBefore: "2026-10-12T00:00:00.000Z",
delivered: 2,
seenAt: [1, 2],
});
assert.deepStrictEqual(
second.items.map((item) => item.number),
[3, 4],
);
assert.include(filters.at(-1), `updated_on <= ${boundary}`);
assert.include(filters.at(-1), "updated_on < 2026-10-12T00:00:00.001Z");
assert.include(filters.at(-1), "id != 1");
assert.include(filters.at(-1), "id != 2");

const last = yield* list({ updatedBefore: older, delivered: 4, seenAt: [4] });
const last = yield* list({
updatedBefore: "2026-10-11T00:00:00.000Z",
delivered: 4,
seenAt: [4],
});
assert.deepStrictEqual(
last.items.map((item) => item.number),
[5],
Expand Down
Loading