@neurowire/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -317,6 +317,55 @@ interface MergeOptions {
317
317
  */
318
318
  declare function mergeFeeds(title: string, parts: MergePart[], options?: MergeOptions): NeurowireFeed;
319
319
 
320
+ /**
321
+ * Pure, deterministic feed refinement: a time-window filter, a sort, and a
322
+ * limit. These operate on the canonical model and never touch the clock or the
323
+ * network themselves. Callers pass `now` (epoch ms) into `resolveWindow`, which
324
+ * keeps the whole module testable and side-effect free.
325
+ */
326
+ type SortKey = 'date' | 'title' | 'source';
327
+ type SortOrder = 'asc' | 'desc';
328
+ interface SelectOptions {
329
+ /** Keep entries dated at or after this epoch-ms bound (inclusive). */
330
+ from?: number;
331
+ /** Keep entries dated at or before this epoch-ms bound (inclusive). */
332
+ to?: number;
333
+ sort?: SortKey;
334
+ order?: SortOrder;
335
+ /** Keep at most this many entries after filtering and sorting. */
336
+ limit?: number;
337
+ }
338
+ /** Parse a duration like "24h", "90m", or "7d" into milliseconds. */
339
+ declare function parseDuration(value: string): number | undefined;
340
+ interface WindowSpec {
341
+ /** Within this duration before now, e.g. "24h". */
342
+ since?: string;
343
+ /** Drop anything older than this duration, e.g. "7d". Same window as `since`. */
344
+ maxAge?: string;
345
+ /** Since midnight UTC today. */
346
+ today?: boolean;
347
+ /** Since midnight UTC of the current week (Monday). */
348
+ thisWeek?: boolean;
349
+ /** An explicit [start, end] pair of ISO dates (or anything Date.parse reads). */
350
+ between?: [string, string];
351
+ }
352
+ interface TimeWindow {
353
+ from?: number;
354
+ to?: number;
355
+ }
356
+ /**
357
+ * Turn a high-level window spec into concrete epoch-ms bounds, relative to
358
+ * `now`. Precedence when several are set: between > today > thisWeek > since/maxAge.
359
+ * Unparseable values yield an open-ended bound on that side.
360
+ */
361
+ declare function resolveWindow(spec: WindowSpec, now: number): TimeWindow;
362
+ /**
363
+ * Apply a date window, an optional sort, and an optional limit to a feed.
364
+ * Date sorts default to newest-first; title and source sorts default to A-Z.
365
+ * Entries without a parseable date are dropped only when a date bound is set.
366
+ */
367
+ declare function selectEntries(feed: NeurowireFeed, opts: SelectOptions): NeurowireFeed;
368
+
320
369
  /** Serialize a feed to an Atom 1.0 document. */
321
370
  declare function toAtom(feed: NeurowireFeed): string;
322
371
 
@@ -385,4 +434,4 @@ declare function isFormat(value: string): value is Format;
385
434
  /** Serialize a feed to the requested format. */
386
435
  declare function serialize(feed: NeurowireFeed, format: Format): string;
387
436
 
388
- export { EXTENSIONS, EntrySchema, FORMATS, FeedSchema, type Format, GENERATOR, type JsonFeedDocument, MEDIA_TYPES, type MergeOptions, type MergePart, type Mesh, MeshSchema, type MeshSource, MeshSourceSchema, type NeurowireEntry, type NeurowireFeed, type NwfIssue, type NwfValidation, type Person, PersonSchema, fromNwf, isFormat, mergeFeeds, parseMesh, parseNeurowireFeed, serialize, toAtom, toJsonFeed, toJsonFeedObject, toMarkdown, toNwf, validateNwf };
437
+ export { EXTENSIONS, EntrySchema, FORMATS, FeedSchema, type Format, GENERATOR, type JsonFeedDocument, MEDIA_TYPES, type MergeOptions, type MergePart, type Mesh, MeshSchema, type MeshSource, MeshSourceSchema, type NeurowireEntry, type NeurowireFeed, type NwfIssue, type NwfValidation, type Person, PersonSchema, type SelectOptions, type SortKey, type SortOrder, type TimeWindow, type WindowSpec, fromNwf, isFormat, mergeFeeds, parseDuration, parseMesh, parseNeurowireFeed, resolveWindow, selectEntries, serialize, toAtom, toJsonFeed, toJsonFeedObject, toMarkdown, toNwf, validateNwf };
package/dist/index.js CHANGED
@@ -83,6 +83,82 @@ function mergeFeeds(title, parts, options = {}) {
83
83
  };
84
84
  }
85
85
 
86
+ // src/refine.ts
87
+ var DAY = 864e5;
88
+ var UNIT_MS = { m: 6e4, h: 36e5, d: 864e5 };
89
+ function parseDuration(value) {
90
+ const match = /^(\d+)\s*([mhd])$/.exec(value.trim());
91
+ if (!match) return void 0;
92
+ return Number(match[1]) * UNIT_MS[match[2]];
93
+ }
94
+ function startOfUtcDay(now) {
95
+ return Math.floor(now / DAY) * DAY;
96
+ }
97
+ function startOfUtcWeek(now) {
98
+ const dayNumber = Math.floor(now / DAY);
99
+ const weekday = ((dayNumber % 7 + 3) % 7 + 7) % 7;
100
+ return (dayNumber - weekday) * DAY;
101
+ }
102
+ function resolveWindow(spec, now) {
103
+ if (spec.between) {
104
+ const from = Date.parse(spec.between[0]);
105
+ const to = Date.parse(spec.between[1]);
106
+ return {
107
+ from: Number.isNaN(from) ? void 0 : from,
108
+ to: Number.isNaN(to) ? void 0 : to
109
+ };
110
+ }
111
+ if (spec.today) return { from: startOfUtcDay(now) };
112
+ if (spec.thisWeek) return { from: startOfUtcWeek(now) };
113
+ const duration = spec.since ?? spec.maxAge;
114
+ if (duration !== void 0) {
115
+ const ms = parseDuration(duration);
116
+ return ms === void 0 ? {} : { from: now - ms };
117
+ }
118
+ return {};
119
+ }
120
+ function entryTime2(entry) {
121
+ return Date.parse(entry.published ?? entry.updated ?? "");
122
+ }
123
+ function compareByTime(a, b) {
124
+ const ta = entryTime2(a);
125
+ const tb = entryTime2(b);
126
+ const aMissing = Number.isNaN(ta);
127
+ const bMissing = Number.isNaN(tb);
128
+ if (aMissing && bMissing) return 0;
129
+ if (aMissing) return -1;
130
+ if (bMissing) return 1;
131
+ return ta - tb;
132
+ }
133
+ function compare(key, a, b) {
134
+ if (key === "title") return a.title.localeCompare(b.title);
135
+ if (key === "source") {
136
+ return (a.source?.name ?? "").localeCompare(b.source?.name ?? "") || compareByTime(a, b);
137
+ }
138
+ return compareByTime(a, b);
139
+ }
140
+ function selectEntries(feed, opts) {
141
+ const { from, to, sort, order, limit } = opts;
142
+ let entries = feed.entries;
143
+ if (from !== void 0 || to !== void 0) {
144
+ entries = entries.filter((entry) => {
145
+ const t = entryTime2(entry);
146
+ if (Number.isNaN(t)) return false;
147
+ if (from !== void 0 && t < from) return false;
148
+ if (to !== void 0 && t > to) return false;
149
+ return true;
150
+ });
151
+ }
152
+ if (sort) {
153
+ const direction = (order ?? (sort === "date" ? "desc" : "asc")) === "asc" ? 1 : -1;
154
+ entries = [...entries].sort((a, b) => direction * compare(sort, a, b));
155
+ }
156
+ if (limit !== void 0 && limit >= 0) {
157
+ entries = entries.slice(0, limit);
158
+ }
159
+ return { ...feed, entries };
160
+ }
161
+
86
162
  // src/serialize/atom.ts
87
163
  var escapeText = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
88
164
  var escapeAttr = (s) => escapeText(s).replace(/"/g, "&quot;");
@@ -582,8 +658,11 @@ export {
582
658
  fromNwf,
583
659
  isFormat,
584
660
  mergeFeeds,
661
+ parseDuration,
585
662
  parseMesh,
586
663
  parseNeurowireFeed,
664
+ resolveWindow,
665
+ selectEntries,
587
666
  serialize,
588
667
  toAtom,
589
668
  toJsonFeed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neurowire/core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Canonical feed model and serializers (Atom, JSON Feed 1.1, Markdown, nwf) for Neurowire.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,7 +38,7 @@
38
38
  "url": "https://github.com/starside-io/neurowire/issues"
39
39
  },
40
40
  "engines": {
41
- "node": ">=20"
41
+ "node": ">=24"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public"