diigo-cache: date windows and search, designed as skipped specs (goal 1d)

The public page's filters, toward the weekly PESOS roundup (#23):
?days=N (7/30/90/365 presets), gmail-style after/before (UTC days,
half-open: after inclusive, before exclusive; yyyy/mm/dd accepted), and
q (words, "phrases", #tags exact against diigo's comma-separated tags,
- negation); q alone searches the last 365 days and says so; malformed
filters are a 400. Filtering walks diigo's updated-desc pages from 1 and
stops at the window, so ?days=7 costs one page.

src/query.ts is the typed, JSDoc'd API (stubs that throw); 22 specs are
describe.skip until it lands. The fake webapp gains a dated account.

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-07 04:19:54 -05:00
1 parent 860bbee2b5
commit 3e5e139dea
4 files changed
+507 -7

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
/**
* @file the public page's filters — goal 1d, **not yet implemented**: the
* design is pinned by the skipped suites in tests/query.test.ts and
* tests/caches.test.ts ("public page filters").
*
* One filter, two spellings: devs pass a {@link FilterSpec} object
* (`{ days: 7, q: "#weekly" }`); URLs carry the same fields as params
* (`?days=7&q=%23weekly`, empty fields = absent). `q` also takes the
* search operators large audiences already know — gmail's `after:`,
* `before:`, `newer_than:7d`; Slack's `during:` (alias `in:`); GitHub's
* `created:`/`updated:` — plus words, `"phrases"`, `#tag`/`tag:tag`, and
* `-` negation.
*
* Time is UTC (Z) throughout; weeks are ISO 8601 (Monday first, so a
* weekend stays together). Every window is half-open: `after` inclusive
* from 00:00Z, `before` exclusive at 00:00Z — so GitHub's inclusive
* `a..b` ranges are deliberately not offered.
*
* Applied while walking diigo's pages (updated-desc). `by=updated` finds
* an old window by binary search over the sorted pages; `by=created`
* walks from page 1 (an old bookmark edited lately sits near page 1).
* Either way a request spends at most {@link FETCH_BUDGET} diigo fetches
* (cached pages are free) and says so when results are incomplete.
*
* @see https://mailmeteor.com/blog/search-gmail-by-date
* @see https://slack.com/help/articles/202528808
* @see https://docs.github.com/en/search-github/searching-on-github/searching-for-repositories
*/
import type { Item } from "./items.js";
/**
* diigo fetches one request may spend (cache hits are free); under the
* Workers free plan's 50 external subrequests per invocation
*
* @see https://developers.cloudflare.com/workers/platform/limits/
*/
export const FETCH_BUDGET = 40;
/**
* a filter as devs write it; URL params map onto these fields one-to-one
* (strings, with "" meaning absent). Dates are `yyyy-mm-dd` (gmail's
* `yyyy/mm/dd` too); periods are `2025`, `2025-03`, `2025-Q1`, or ISO week
* `2025-W01`.
*/
export type FilterSpec = {
/** the last N days, counted back from now (1–3650) */
days?: number | string;
/** inclusive lower bound, from 00:00Z */
after?: string;
/** exclusive upper bound, at 00:00Z */
before?: string;
/** a named period: shorthand for its after/before pair */
in?: string;
/** which date the window, the listed date, and the sort use */
by?: "updated" | "created" | "";
/** words, phrases, tags, and operators */
q?: string;
};
/** one search term; `negated` for a leading `-` */
export type Term = { text: string; negated: boolean };
/** a parsed filter; an empty one (no window, no terms) filters nothing */
export type Query = {
/** which date the window, the listed date, and the sort use (default
* `updated`, diigo's own order) */
by: "updated" | "created";
/** inclusive lower bound */
after?: Date;
/** exclusive upper bound */
before?: Date;
/** words and phrases: each must appear in title, description, or url */
words: Term[];
/** tags: exact, case-insensitive match against the item's tags */
tags: Term[];
};
/** a refused filter: the message is safe to show and log */
export type QueryError = { error: string };
/**
* resolve a filter: fields and `q` operators combine; a bound given twice
* (by fields, operators, or both) is refused, as is an empty window.
*
* @param now the clock `days`/`newer_than` count back from (passed in,
* not read ambiently, so tests pin it)
*/
export function toQuery(spec: FilterSpec, now: Date): Query | QueryError {
void spec;
void now;
throw new Error("toQuery: not implemented (goal 1d)");
}
/** {@link toQuery} over the public page's URL params */
export function parseQuery(params: URLSearchParams, now: Date): Query | QueryError {
void params;
void now;
throw new Error("parseQuery: not implemented (goal 1d)");
}
/** does `item` fall in the window and satisfy every term? */
export function matches(item: Item, query: Query): boolean {
void item;
void query;
throw new Error("matches: not implemented (goal 1d)");
}