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:
1 parent
860bbee2b5
commit
3e5e139dea
4 files changed
+507
-7
No files matched your search
@@ -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)");
|
||||
}
|
||||
Reference in new issue
Block a user