Feeds & Series API#
This document covers the APIs for time-series data and external feeds in Peek.
Overview#
Peek uses two item types for temporal data:
series- Scalar values over time (numeric or text)feed- Entries from external sources (RSS, APIs, logs)
Both store their data in the item_events table. The item defines what you're tracking; events record when things happened.
Core Concepts#
Items = Definitions#
Items with type: 'series' or type: 'feed' define what you're tracking:
// A series tracks scalar values
const series = await api.datastore.addItem('series', {
content: 'AAPL Stock Price',
metadata: JSON.stringify({ unit: 'USD', source: 'yahoo-finance' })
});
// A feed tracks entries from a source
const feed = await api.datastore.addItem('feed', {
content: 'https://example.com/rss',
metadata: JSON.stringify({ name: 'Example Blog', format: 'rss' })
});
Events = Facts#
Events are append-only records of what happened:
// Record a series observation
await api.datastore.addItemEvent(series.data.id, {
value: 156.32, // numeric value
occurredAt: Date.now()
});
// Record a feed entry
await api.datastore.addItemEvent(feed.data.id, {
content: 'https://example.com/article-123', // entry URL
occurredAt: pubDate,
metadata: JSON.stringify({ title: 'Article Title', author: 'Jane' })
});
API Reference#
Item Operations#
Standard item CRUD works for series and feeds:
// Create
const result = await api.datastore.addItem('series', { content: 'Mood', metadata: '{}' });
// Read
const item = await api.datastore.getItem(itemId);
// Update
await api.datastore.updateItem(itemId, { metadata: '{"unit":"points"}' });
// Delete (also delete events first)
await api.datastore.deleteItemEvents(itemId);
await api.datastore.deleteItem(itemId);
// Query all series
const series = await api.datastore.queryItems({ type: 'series' });
// Query all feeds
const feeds = await api.datastore.queryItems({ type: 'feed' });
Event Operations#
addItemEvent(itemId, options)#
Add an event to a series or feed.
await api.datastore.addItemEvent(itemId, {
content: 'text value or URL', // optional
value: 42, // optional, for numeric data
occurredAt: Date.now(), // optional, defaults to now
metadata: JSON.stringify({...}) // optional, JSON string
});
getItemEvent(eventId)#
Get a single event by ID.
const event = await api.datastore.getItemEvent(eventId);
// { id, itemId, content, value, occurredAt, metadata, createdAt }
queryItemEvents(filter)#
Query events with filters.
const events = await api.datastore.queryItemEvents({
itemId: 'item_123', // single item
// OR
itemIds: ['item_1', 'item_2'], // multiple items
since: Date.now() - 86400000, // after timestamp
until: Date.now(), // before timestamp
limit: 50, // max results
offset: 0, // skip first N
order: 'desc' // 'asc' or 'desc' by occurredAt
});
deleteItemEvent(eventId)#
Delete a single event.
await api.datastore.deleteItemEvent(eventId);
deleteItemEvents(itemId)#
Delete all events for an item.
const count = await api.datastore.deleteItemEvents(itemId);
getLatestItemEvent(itemId)#
Get the most recent event.
const latest = await api.datastore.getLatestItemEvent(itemId);
countItemEvents(itemId, filter)#
Count events for an item.
const count = await api.datastore.countItemEvents(itemId, {
since: Date.now() - 86400000 // last 24 hours
});
Use-Case Examples#
1. Stock Price Tracking#
Track numeric values over time from a web source.
// Create the series
const stock = await api.datastore.addItem('series', {
content: 'AAPL',
metadata: JSON.stringify({
name: 'Apple Stock Price',
unit: 'USD',
source_url: 'https://finance.yahoo.com/quote/AAPL'
})
});
const stockId = stock.data.id;
// Record observations (from polling job)
async function recordPrice(price) {
await api.datastore.addItemEvent(stockId, {
value: price,
occurredAt: Date.now()
});
}
// Query last 7 days
const prices = await api.datastore.queryItemEvents({
itemId: stockId,
since: Date.now() - 7 * 24 * 60 * 60 * 1000,
order: 'asc'
});
// Calculate average
const avg = prices.data.reduce((sum, e) => sum + e.value, 0) / prices.data.length;
// Get latest price
const latest = await api.datastore.getLatestItemEvent(stockId);
console.log(`Current: $${latest.data.value}, 7-day avg: $${avg.toFixed(2)}`);
2. Mood Tracking (Text Values)#
Track non-numeric values like mood or status.
// Create the series
const mood = await api.datastore.addItem('series', {
content: 'Mood',
metadata: JSON.stringify({ input: 'tag-board' })
});
const moodId = mood.data.id;
// Record mood (from tag button board)
async function recordMood(moodValue) {
await api.datastore.addItemEvent(moodId, {
content: moodValue, // 'great', 'good', 'meh', 'bad'
occurredAt: Date.now()
});
}
// Usage
await recordMood('great');
await recordMood('meh');
// Query this week's moods
const moods = await api.datastore.queryItemEvents({
itemId: moodId,
since: Date.now() - 7 * 24 * 60 * 60 * 1000
});
// Count by mood
const counts = {};
for (const event of moods.data) {
counts[event.content] = (counts[event.content] || 0) + 1;
}
console.log('Mood distribution:', counts);
3. Habit Tracking with Streaks#
Track daily habits and calculate streaks.
// Create the series
const pushups = await api.datastore.addItem('series', {
content: 'Pushups',
metadata: JSON.stringify({ frequency: 'daily', tag: 'pushups' })
});
const pushupId = pushups.data.id;
// Record daily check-in
async function logPushups(count) {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
await api.datastore.addItemEvent(pushupId, {
value: count,
occurredAt: Date.now(),
metadata: JSON.stringify({ date: today })
});
}
// Calculate current streak
async function getStreak() {
const events = await api.datastore.queryItemEvents({
itemId: pushupId,
limit: 365,
order: 'desc'
});
if (!events.success || events.data.length === 0) return 0;
let streak = 0;
let expectedDate = new Date();
expectedDate.setHours(0, 0, 0, 0);
for (const event of events.data) {
const meta = JSON.parse(event.metadata || '{}');
const eventDate = new Date(meta.date);
eventDate.setHours(0, 0, 0, 0);
// Check if this is the expected date (consecutive day)
const diffDays = Math.round((expectedDate - eventDate) / 86400000);
if (diffDays === 0) {
streak++;
expectedDate.setDate(expectedDate.getDate() - 1);
} else if (diffDays === 1) {
// Skip today if not logged yet
expectedDate.setDate(expectedDate.getDate() - 1);
if (expectedDate.getTime() === eventDate.getTime()) {
streak++;
expectedDate.setDate(expectedDate.getDate() - 1);
} else {
break;
}
} else {
break;
}
}
return streak;
}
// Usage
await logPushups(20);
const streak = await getStreak();
console.log(`Current streak: ${streak} days`);
// Get personal best
const allEvents = await api.datastore.queryItemEvents({ itemId: pushupId });
const max = Math.max(...allEvents.data.map(e => e.value || 0));
console.log(`Personal best: ${max} pushups`);
4. Ticket Availability Trigger#
Poll a page and notify when condition is met.
// Create the series (acts as trigger definition)
const tickets = await api.datastore.addItem('series', {
content: 'Taylor Swift Tickets',
metadata: JSON.stringify({
source_url: 'https://ticketmaster.com/event/123',
selector: '.buy-button',
condition: 'exists',
notified: false
})
});
const ticketId = tickets.data.id;
// Polling function (run periodically)
async function checkTickets() {
const item = await api.datastore.getItem(ticketId);
const meta = JSON.parse(item.data.metadata || '{}');
if (meta.notified) return; // Already notified
// In a real extension, you'd load the page in a hidden window
// and run document.querySelector(meta.selector)
const available = await checkPageForElement(meta.source_url, meta.selector);
// Record the check
await api.datastore.addItemEvent(ticketId, {
value: available ? 1 : 0,
content: available ? 'Available!' : 'Not available',
occurredAt: Date.now()
});
// Trigger notification
if (available) {
meta.notified = true;
await api.datastore.updateItem(ticketId, {
metadata: JSON.stringify(meta)
});
// Show notification
new Notification('Tickets Available!', {
body: 'Taylor Swift tickets are now on sale!'
});
}
}
5. RSS Feed Reader#
Subscribe to and poll RSS feeds.
// Subscribe to a feed
async function subscribe(url) {
const response = await fetch(url);
const xml = await response.text();
const title = parseFeedTitle(xml); // Your XML parsing function
const feed = await api.datastore.addItem('feed', {
content: url,
metadata: JSON.stringify({
name: title,
format: 'rss',
lastPolledAt: 0
})
});
return feed.data.id;
}
// Poll a feed for new entries
async function pollFeed(feedId) {
const item = await api.datastore.getItem(feedId);
const feedUrl = item.data.content;
const response = await fetch(feedUrl);
const xml = await response.text();
const entries = parseFeedEntries(xml); // Your XML parsing function
// Get existing entries to dedupe
const existing = await api.datastore.queryItemEvents({ itemId: feedId, limit: 100 });
const existingGuids = new Set(
existing.data.map(e => JSON.parse(e.metadata || '{}').guid)
);
// Add new entries
for (const entry of entries) {
if (existingGuids.has(entry.guid)) continue;
await api.datastore.addItemEvent(feedId, {
content: entry.link,
occurredAt: entry.pubDate,
metadata: JSON.stringify({
guid: entry.guid,
title: entry.title,
author: entry.author
})
});
}
// Update last polled
const meta = JSON.parse(item.data.metadata || '{}');
meta.lastPolledAt = Date.now();
await api.datastore.updateItem(feedId, { metadata: JSON.stringify(meta) });
}
// Get all entries across feeds
async function getAllEntries(limit = 50) {
const feeds = await api.datastore.queryItems({ type: 'feed' });
const feedIds = feeds.data.map(f => f.id);
const entries = await api.datastore.queryItemEvents({
itemIds: feedIds,
limit: limit,
order: 'desc'
});
return entries.data.map(e => ({
...e,
meta: JSON.parse(e.metadata || '{}')
}));
}
6. System Metrics / Action Log#
Track internal application events.
// Create a log feed
const actionLog = await api.datastore.addItem('feed', {
content: 'peek:system:actions',
metadata: JSON.stringify({ format: 'internal' })
});
const logId = actionLog.data.id;
// Log an action
async function logAction(action, details = {}) {
await api.datastore.addItemEvent(logId, {
content: action,
occurredAt: Date.now(),
metadata: JSON.stringify(details)
});
}
// Usage
await logAction('page_open', { url: 'https://example.com', source: 'bookmark' });
await logAction('settings_changed', { key: 'theme', value: 'dark' });
await logAction('extension_loaded', { id: 'feeds', version: '1.0.0' });
// Query recent actions
const recent = await api.datastore.queryItemEvents({
itemId: logId,
since: Date.now() - 3600000, // last hour
order: 'desc'
});
// Count actions by type
const counts = {};
for (const event of recent.data) {
counts[event.content] = (counts[event.content] || 0) + 1;
}
console.log('Action counts:', counts);
7. Spotify Listening History#
Pull data from external APIs.
// Create the feed
const spotify = await api.datastore.addItem('feed', {
content: 'spotify:me:recently-played',
metadata: JSON.stringify({
format: 'api',
service: 'spotify',
lastSyncedAt: 0
})
});
const spotifyId = spotify.data.id;
// Sync from Spotify API (requires OAuth token)
async function syncSpotify(accessToken) {
const response = await fetch('https://api.spotify.com/v1/me/player/recently-played?limit=50', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const data = await response.json();
// Get existing to dedupe
const existing = await api.datastore.queryItemEvents({ itemId: spotifyId, limit: 100 });
const existingIds = new Set(
existing.data.map(e => JSON.parse(e.metadata || '{}').playedAt)
);
// Add new plays
for (const item of data.items) {
const playedAt = new Date(item.played_at).getTime();
if (existingIds.has(item.played_at)) continue;
await api.datastore.addItemEvent(spotifyId, {
content: item.track.uri,
occurredAt: playedAt,
metadata: JSON.stringify({
playedAt: item.played_at,
trackName: item.track.name,
artistName: item.track.artists[0]?.name,
albumName: item.track.album?.name,
durationMs: item.track.duration_ms
})
});
}
// Update sync time
const meta = JSON.parse((await api.datastore.getItem(spotifyId)).data.metadata || '{}');
meta.lastSyncedAt = Date.now();
await api.datastore.updateItem(spotifyId, { metadata: JSON.stringify(meta) });
}
// Get top artists from listening history
async function getTopArtists(days = 30) {
const plays = await api.datastore.queryItemEvents({
itemId: spotifyId,
since: Date.now() - days * 24 * 60 * 60 * 1000
});
const artistCounts = {};
for (const play of plays.data) {
const meta = JSON.parse(play.metadata || '{}');
const artist = meta.artistName || 'Unknown';
artistCounts[artist] = (artistCounts[artist] || 0) + 1;
}
return Object.entries(artistCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
}
Schema Reference#
items table (existing, extended)#
items (
id TEXT PRIMARY KEY,
type TEXT NOT NULL, -- 'url', 'text', 'tagset', 'image', 'series', 'feed'
content TEXT, -- identifier, URL, or name
metadata TEXT, -- JSON with type-specific data
...
)
item_events table (new)#
item_events (
id TEXT PRIMARY KEY,
itemId TEXT NOT NULL, -- FK to series or feed
content TEXT, -- text value, URL, or message
value REAL, -- numeric value (optional)
occurredAt INTEGER, -- when it happened (Unix ms)
metadata TEXT, -- JSON extras
createdAt INTEGER -- when recorded
)
-- Indexes
CREATE INDEX idx_item_events_item_time ON item_events(itemId, occurredAt DESC);
CREATE INDEX idx_item_events_occurred ON item_events(occurredAt DESC);
Best Practices#
-
Use
valuefor numbers,contentfor text/URLs - Makes queries and aggregations simpler. -
Store dedup keys in metadata - For feeds, store
guidto avoid duplicate entries. -
Set
occurredAtto event time, not record time - For RSS entries, usepubDate; for API data, use the original timestamp. -
Keep metadata lightweight - Truncate descriptions, store only what you need for display.
-
Delete events when deleting items - Always call
deleteItemEvents(itemId)beforedeleteItem(itemId). -
Use
order: 'desc'for recent-first - Most UIs want newest first. -
Batch queries with
itemIds- When showing entries from multiple feeds, query once with all IDs.