@dwk/microsub 0.1.0-beta.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/LICENSE +15 -0
- package/README.md +92 -0
- package/dist/auth.d.ts +53 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +102 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +102 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/consumer.d.ts +40 -0
- package/dist/consumer.d.ts.map +1 -0
- package/dist/consumer.js +87 -0
- package/dist/consumer.js.map +1 -0
- package/dist/discovery.d.ts +59 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +190 -0
- package/dist/discovery.js.map +1 -0
- package/dist/fetch.d.ts +28 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +72 -0
- package/dist/fetch.js.map +1 -0
- package/dist/handler.d.ts +24 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +434 -0
- package/dist/handler.js.map +1 -0
- package/dist/hfeed.d.ts +25 -0
- package/dist/hfeed.d.ts.map +1 -0
- package/dist/hfeed.js +252 -0
- package/dist/hfeed.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/jf2.d.ts +69 -0
- package/dist/jf2.d.ts.map +1 -0
- package/dist/jf2.js +295 -0
- package/dist/jf2.js.map +1 -0
- package/dist/log.d.ts +44 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +42 -0
- package/dist/log.js.map +1 -0
- package/dist/poll.d.ts +22 -0
- package/dist/poll.d.ts.map +1 -0
- package/dist/poll.js +39 -0
- package/dist/poll.js.map +1 -0
- package/dist/queue.d.ts +25 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +13 -0
- package/dist/queue.js.map +1 -0
- package/dist/replay.d.ts +34 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +49 -0
- package/dist/replay.js.map +1 -0
- package/dist/safe-fetch.d.ts +86 -0
- package/dist/safe-fetch.d.ts.map +1 -0
- package/dist/safe-fetch.js +311 -0
- package/dist/safe-fetch.js.map +1 -0
- package/dist/store.d.ts +131 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +393 -0
- package/dist/store.js.map +1 -0
- package/dist/xml.d.ts +51 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +196 -0
- package/dist/xml.js.map +1 -0
- package/package.json +49 -0
- package/src/auth.ts +184 -0
- package/src/config.ts +156 -0
- package/src/consumer.ts +140 -0
- package/src/discovery.ts +270 -0
- package/src/fetch.ts +82 -0
- package/src/handler.ts +594 -0
- package/src/hfeed.ts +287 -0
- package/src/index.ts +86 -0
- package/src/jf2.ts +394 -0
- package/src/log.ts +46 -0
- package/src/poll.ts +72 -0
- package/src/queue.ts +26 -0
- package/src/replay.ts +68 -0
- package/src/safe-fetch.ts +346 -0
- package/src/store.ts +644 -0
- package/src/xml.ts +229 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/microsub` — D1-backed authoritative state.
|
|
3
|
+
*
|
|
4
|
+
* Channels, the feeds each channel follows, the normalised JF2 timeline items,
|
|
5
|
+
* their per-item read flags, and a small per-feed poll cache (`ETag` /
|
|
6
|
+
* `Last-Modified`) all live here. This is correctness-sensitive state — a lost
|
|
7
|
+
* subscription or a dropped read flag is a bug, not a stale cache — so it lives
|
|
8
|
+
* in D1, a strongly-consistent store, and **never** KV (see
|
|
9
|
+
* `spec/non-functional-requirements.md`). The schema is created lazily on first
|
|
10
|
+
* use.
|
|
11
|
+
*
|
|
12
|
+
* Timeline paging uses a monotonic `seq` (an autoincrement rowid): the opaque
|
|
13
|
+
* `before` / `after` cursors a Microsub client round-trips are just `seq`
|
|
14
|
+
* values, so paging is stable under concurrent inserts and deletes.
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
/** The reserved channel that always exists and cannot be deleted or renamed. */
|
|
19
|
+
export const NOTIFICATIONS_CHANNEL = "notifications";
|
|
20
|
+
const SCHEMA = [
|
|
21
|
+
`CREATE TABLE IF NOT EXISTS microsub_channels (
|
|
22
|
+
uid TEXT PRIMARY KEY,
|
|
23
|
+
name TEXT NOT NULL,
|
|
24
|
+
position INTEGER NOT NULL,
|
|
25
|
+
created_at INTEGER NOT NULL
|
|
26
|
+
)`,
|
|
27
|
+
`CREATE TABLE IF NOT EXISTS microsub_follows (
|
|
28
|
+
channel TEXT NOT NULL,
|
|
29
|
+
feed_url TEXT NOT NULL,
|
|
30
|
+
page_url TEXT NOT NULL,
|
|
31
|
+
created_at INTEGER NOT NULL,
|
|
32
|
+
PRIMARY KEY (channel, feed_url)
|
|
33
|
+
)`,
|
|
34
|
+
`CREATE TABLE IF NOT EXISTS microsub_items (
|
|
35
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
channel TEXT NOT NULL,
|
|
37
|
+
entry_id TEXT NOT NULL,
|
|
38
|
+
feed_url TEXT NOT NULL,
|
|
39
|
+
published INTEGER,
|
|
40
|
+
is_read INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
data TEXT NOT NULL,
|
|
42
|
+
created_at INTEGER NOT NULL,
|
|
43
|
+
UNIQUE (channel, entry_id)
|
|
44
|
+
)`,
|
|
45
|
+
`CREATE INDEX IF NOT EXISTS microsub_items_channel_seq
|
|
46
|
+
ON microsub_items (channel, seq)`,
|
|
47
|
+
`CREATE TABLE IF NOT EXISTS microsub_feeds (
|
|
48
|
+
feed_url TEXT PRIMARY KEY,
|
|
49
|
+
etag TEXT,
|
|
50
|
+
last_modified TEXT,
|
|
51
|
+
last_polled INTEGER
|
|
52
|
+
)`,
|
|
53
|
+
];
|
|
54
|
+
function channelFromRow(row) {
|
|
55
|
+
return {
|
|
56
|
+
uid: row.uid,
|
|
57
|
+
name: row.name,
|
|
58
|
+
position: row.position,
|
|
59
|
+
createdAt: row.created_at,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function itemFromRow(row) {
|
|
63
|
+
const entry = JSON.parse(row.data);
|
|
64
|
+
return {
|
|
65
|
+
seq: row.seq,
|
|
66
|
+
channel: row.channel,
|
|
67
|
+
entryId: row.entry_id,
|
|
68
|
+
feedUrl: row.feed_url,
|
|
69
|
+
isRead: row.is_read !== 0,
|
|
70
|
+
entry: { ...entry, _is_read: row.is_read !== 0 },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Parse a `published` ISO string to an epoch-seconds sort key, or `null`. */
|
|
74
|
+
function publishedToEpoch(entry) {
|
|
75
|
+
if (!entry.published)
|
|
76
|
+
return null;
|
|
77
|
+
const ms = Date.parse(entry.published);
|
|
78
|
+
return Number.isFinite(ms) ? Math.floor(ms / 1000) : null;
|
|
79
|
+
}
|
|
80
|
+
/** A short, URL-safe channel uid: base36 timestamp plus random suffix. */
|
|
81
|
+
function generateUid() {
|
|
82
|
+
const time = Date.now().toString(36);
|
|
83
|
+
const rand = Math.floor(Math.random() * 36 ** 5)
|
|
84
|
+
.toString(36)
|
|
85
|
+
.padStart(5, "0");
|
|
86
|
+
return `${time}${rand}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create the D1-backed {@link MicrosubStore}. Fails loudly if the required
|
|
90
|
+
* `MICROSUB_DB` binding is missing — no silent degradation (composition
|
|
91
|
+
* contract).
|
|
92
|
+
*/
|
|
93
|
+
export function createMicrosubStore(env) {
|
|
94
|
+
if (!env.MICROSUB_DB) {
|
|
95
|
+
throw new Error("@dwk/microsub: missing required D1 binding `MICROSUB_DB`");
|
|
96
|
+
}
|
|
97
|
+
const db = env.MICROSUB_DB;
|
|
98
|
+
// The init promise is cached per store instance (not module-globally): the
|
|
99
|
+
// vitest-pool-workers test model resets D1 between tests while keeping one
|
|
100
|
+
// `env` singleton, so a global cache would skip re-creating the wiped tables.
|
|
101
|
+
// This matches `@dwk/websub`'s store. The reserved `notifications` channel is
|
|
102
|
+
// materialised as part of the same batch, so it always exists after any store
|
|
103
|
+
// operation — a client may hit `timeline`/`unread` for it on a fresh DB
|
|
104
|
+
// without first listing channels.
|
|
105
|
+
let ready = null;
|
|
106
|
+
const ensureSchema = () => {
|
|
107
|
+
// Clear the cached promise on failure so a transient D1 error during the
|
|
108
|
+
// first call doesn't permanently wedge the store.
|
|
109
|
+
ready ??= db
|
|
110
|
+
.batch([
|
|
111
|
+
...SCHEMA.map((ddl) => db.prepare(ddl)),
|
|
112
|
+
db
|
|
113
|
+
.prepare(`INSERT OR IGNORE INTO microsub_channels (uid, name, position, created_at)
|
|
114
|
+
VALUES (?1, ?2, -1, ?3)`)
|
|
115
|
+
.bind(NOTIFICATIONS_CHANNEL, "Notifications", Math.floor(Date.now() / 1000)),
|
|
116
|
+
])
|
|
117
|
+
.then(() => undefined)
|
|
118
|
+
.catch((err) => {
|
|
119
|
+
ready = null;
|
|
120
|
+
throw err;
|
|
121
|
+
});
|
|
122
|
+
return ready;
|
|
123
|
+
};
|
|
124
|
+
const seqOf = async (channel, entryId) => {
|
|
125
|
+
const row = await db
|
|
126
|
+
.prepare(`SELECT seq FROM microsub_items WHERE channel = ?1 AND entry_id = ?2`)
|
|
127
|
+
.bind(channel, entryId)
|
|
128
|
+
.first();
|
|
129
|
+
return row?.seq ?? null;
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
async init() {
|
|
133
|
+
await ensureSchema();
|
|
134
|
+
},
|
|
135
|
+
// Channels ------------------------------------------------------------
|
|
136
|
+
async listChannels() {
|
|
137
|
+
await ensureSchema();
|
|
138
|
+
const { results } = await db
|
|
139
|
+
.prepare(`SELECT uid, name, position, created_at FROM microsub_channels
|
|
140
|
+
ORDER BY position ASC, created_at ASC`)
|
|
141
|
+
.all();
|
|
142
|
+
return results.map(channelFromRow);
|
|
143
|
+
},
|
|
144
|
+
async channelExists(uid) {
|
|
145
|
+
await ensureSchema();
|
|
146
|
+
const row = await db
|
|
147
|
+
.prepare(`SELECT 1 AS present FROM microsub_channels WHERE uid = ?1`)
|
|
148
|
+
.bind(uid)
|
|
149
|
+
.first();
|
|
150
|
+
return row !== null;
|
|
151
|
+
},
|
|
152
|
+
async createChannel(name, now) {
|
|
153
|
+
await ensureSchema();
|
|
154
|
+
const max = await db
|
|
155
|
+
.prepare(`SELECT MAX(position) AS maxPos FROM microsub_channels`)
|
|
156
|
+
.first();
|
|
157
|
+
const position = (max?.maxPos ?? -1) + 1;
|
|
158
|
+
const uid = generateUid();
|
|
159
|
+
await db
|
|
160
|
+
.prepare(`INSERT INTO microsub_channels (uid, name, position, created_at)
|
|
161
|
+
VALUES (?1, ?2, ?3, ?4)`)
|
|
162
|
+
.bind(uid, name, position, now)
|
|
163
|
+
.run();
|
|
164
|
+
return { uid, name, position, createdAt: now };
|
|
165
|
+
},
|
|
166
|
+
async renameChannel(uid, name) {
|
|
167
|
+
await ensureSchema();
|
|
168
|
+
if (uid === NOTIFICATIONS_CHANNEL)
|
|
169
|
+
return null;
|
|
170
|
+
const result = await db
|
|
171
|
+
.prepare(`UPDATE microsub_channels SET name = ?2 WHERE uid = ?1`)
|
|
172
|
+
.bind(uid, name)
|
|
173
|
+
.run();
|
|
174
|
+
if (result.meta.changes === 0)
|
|
175
|
+
return null;
|
|
176
|
+
const row = await db
|
|
177
|
+
.prepare(`SELECT uid, name, position, created_at FROM microsub_channels WHERE uid = ?1`)
|
|
178
|
+
.bind(uid)
|
|
179
|
+
.first();
|
|
180
|
+
return row ? channelFromRow(row) : null;
|
|
181
|
+
},
|
|
182
|
+
async deleteChannel(uid) {
|
|
183
|
+
await ensureSchema();
|
|
184
|
+
if (uid === NOTIFICATIONS_CHANNEL)
|
|
185
|
+
return false;
|
|
186
|
+
const result = await db.batch([
|
|
187
|
+
db.prepare(`DELETE FROM microsub_channels WHERE uid = ?1`).bind(uid),
|
|
188
|
+
db.prepare(`DELETE FROM microsub_follows WHERE channel = ?1`).bind(uid),
|
|
189
|
+
db.prepare(`DELETE FROM microsub_items WHERE channel = ?1`).bind(uid),
|
|
190
|
+
]);
|
|
191
|
+
return (result[0]?.meta.changes ?? 0) > 0;
|
|
192
|
+
},
|
|
193
|
+
async reorderChannels(order) {
|
|
194
|
+
await ensureSchema();
|
|
195
|
+
const statements = order.map((uid, index) => db
|
|
196
|
+
.prepare(`UPDATE microsub_channels SET position = ?2 WHERE uid = ?1`)
|
|
197
|
+
.bind(uid, index));
|
|
198
|
+
if (statements.length > 0)
|
|
199
|
+
await db.batch(statements);
|
|
200
|
+
},
|
|
201
|
+
// Follows -------------------------------------------------------------
|
|
202
|
+
async listFollows(channel) {
|
|
203
|
+
await ensureSchema();
|
|
204
|
+
const { results } = await db
|
|
205
|
+
.prepare(`SELECT feed_url, page_url, created_at FROM microsub_follows
|
|
206
|
+
WHERE channel = ?1 ORDER BY created_at ASC`)
|
|
207
|
+
.bind(channel)
|
|
208
|
+
.all();
|
|
209
|
+
return results.map((row) => ({
|
|
210
|
+
feedUrl: row.feed_url,
|
|
211
|
+
pageUrl: row.page_url,
|
|
212
|
+
createdAt: row.created_at,
|
|
213
|
+
}));
|
|
214
|
+
},
|
|
215
|
+
async addFollow(channel, feedUrl, pageUrl, now) {
|
|
216
|
+
await ensureSchema();
|
|
217
|
+
await db
|
|
218
|
+
.prepare(`INSERT INTO microsub_follows (channel, feed_url, page_url, created_at)
|
|
219
|
+
VALUES (?1, ?2, ?3, ?4)
|
|
220
|
+
ON CONFLICT (channel, feed_url) DO UPDATE SET page_url = excluded.page_url`)
|
|
221
|
+
.bind(channel, feedUrl, pageUrl, now)
|
|
222
|
+
.run();
|
|
223
|
+
},
|
|
224
|
+
async removeFollow(channel, url) {
|
|
225
|
+
await ensureSchema();
|
|
226
|
+
const result = await db
|
|
227
|
+
.prepare(`DELETE FROM microsub_follows
|
|
228
|
+
WHERE channel = ?1 AND (feed_url = ?2 OR page_url = ?2)`)
|
|
229
|
+
.bind(channel, url)
|
|
230
|
+
.run();
|
|
231
|
+
return (result.meta.changes ?? 0) > 0;
|
|
232
|
+
},
|
|
233
|
+
async distinctFeedUrls() {
|
|
234
|
+
await ensureSchema();
|
|
235
|
+
const { results } = await db
|
|
236
|
+
.prepare(`SELECT DISTINCT feed_url FROM microsub_follows`)
|
|
237
|
+
.all();
|
|
238
|
+
return results.map((row) => row.feed_url);
|
|
239
|
+
},
|
|
240
|
+
async channelsForFeed(feedUrl) {
|
|
241
|
+
await ensureSchema();
|
|
242
|
+
const { results } = await db
|
|
243
|
+
.prepare(`SELECT channel FROM microsub_follows WHERE feed_url = ?1`)
|
|
244
|
+
.bind(feedUrl)
|
|
245
|
+
.all();
|
|
246
|
+
return results.map((row) => row.channel);
|
|
247
|
+
},
|
|
248
|
+
// Items ---------------------------------------------------------------
|
|
249
|
+
async insertItems(channel, feedUrl, entries, now, maxItems) {
|
|
250
|
+
await ensureSchema();
|
|
251
|
+
if (entries.length === 0)
|
|
252
|
+
return 0;
|
|
253
|
+
const statements = entries.map((entry) => db
|
|
254
|
+
.prepare(`INSERT OR IGNORE INTO microsub_items
|
|
255
|
+
(channel, entry_id, feed_url, published, is_read, data, created_at)
|
|
256
|
+
VALUES (?1, ?2, ?3, ?4, 0, ?5, ?6)`)
|
|
257
|
+
.bind(channel, entry._id, feedUrl, publishedToEpoch(entry), JSON.stringify(entry), now));
|
|
258
|
+
const results = await db.batch(statements);
|
|
259
|
+
const added = results.reduce((sum, r) => sum + (r.meta.changes ?? 0), 0);
|
|
260
|
+
// Retention: reap the oldest items past the ceiling so a runaway feed
|
|
261
|
+
// can't fill the store unbounded.
|
|
262
|
+
if (added > 0) {
|
|
263
|
+
await db
|
|
264
|
+
.prepare(`DELETE FROM microsub_items
|
|
265
|
+
WHERE channel = ?1 AND seq NOT IN (
|
|
266
|
+
SELECT seq FROM microsub_items WHERE channel = ?1
|
|
267
|
+
ORDER BY seq DESC LIMIT ?2
|
|
268
|
+
)`)
|
|
269
|
+
.bind(channel, maxItems)
|
|
270
|
+
.run();
|
|
271
|
+
}
|
|
272
|
+
return added;
|
|
273
|
+
},
|
|
274
|
+
async listItems(channel, options) {
|
|
275
|
+
await ensureSchema();
|
|
276
|
+
const limit = Math.max(1, options.limit);
|
|
277
|
+
let rows;
|
|
278
|
+
if (options.after !== undefined) {
|
|
279
|
+
const after = Number.parseInt(options.after, 10);
|
|
280
|
+
const { results } = await db
|
|
281
|
+
.prepare(`SELECT seq, channel, entry_id, feed_url, is_read, data
|
|
282
|
+
FROM microsub_items WHERE channel = ?1 AND seq > ?2
|
|
283
|
+
ORDER BY seq ASC LIMIT ?3`)
|
|
284
|
+
.bind(channel, Number.isFinite(after) ? after : 0, limit)
|
|
285
|
+
.all();
|
|
286
|
+
rows = results.slice().reverse();
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const before = options.before !== undefined
|
|
290
|
+
? Number.parseInt(options.before, 10)
|
|
291
|
+
: Number.MAX_SAFE_INTEGER;
|
|
292
|
+
const { results } = await db
|
|
293
|
+
.prepare(`SELECT seq, channel, entry_id, feed_url, is_read, data
|
|
294
|
+
FROM microsub_items WHERE channel = ?1 AND seq < ?2
|
|
295
|
+
ORDER BY seq DESC LIMIT ?3`)
|
|
296
|
+
.bind(channel, Number.isFinite(before) ? before : Number.MAX_SAFE_INTEGER, limit)
|
|
297
|
+
.all();
|
|
298
|
+
rows = results;
|
|
299
|
+
}
|
|
300
|
+
const items = rows.map(itemFromRow);
|
|
301
|
+
const page = {
|
|
302
|
+
items,
|
|
303
|
+
};
|
|
304
|
+
if (items.length > 0) {
|
|
305
|
+
const last = items[items.length - 1];
|
|
306
|
+
const first = items[0];
|
|
307
|
+
// `before` advances to older entries. Offer it when the page filled
|
|
308
|
+
// (there may be more below), and always when paginating with `after`:
|
|
309
|
+
// even a partial newer-page sits above the entries we paged up from, so
|
|
310
|
+
// the client must be able to navigate back down. `after` always points
|
|
311
|
+
// past the newest seen.
|
|
312
|
+
if ((items.length === limit || options.after !== undefined) && last) {
|
|
313
|
+
page.before = String(last.seq);
|
|
314
|
+
}
|
|
315
|
+
if (first)
|
|
316
|
+
page.after = String(first.seq);
|
|
317
|
+
}
|
|
318
|
+
return page;
|
|
319
|
+
},
|
|
320
|
+
async unreadCount(channel) {
|
|
321
|
+
await ensureSchema();
|
|
322
|
+
const row = await db
|
|
323
|
+
.prepare(`SELECT COUNT(*) AS n FROM microsub_items WHERE channel = ?1 AND is_read = 0`)
|
|
324
|
+
.bind(channel)
|
|
325
|
+
.first();
|
|
326
|
+
return row?.n ?? 0;
|
|
327
|
+
},
|
|
328
|
+
async markRead(channel, entryIds, read) {
|
|
329
|
+
await ensureSchema();
|
|
330
|
+
if (entryIds.length === 0)
|
|
331
|
+
return;
|
|
332
|
+
const flag = read ? 1 : 0;
|
|
333
|
+
const statements = entryIds.map((id) => db
|
|
334
|
+
.prepare(`UPDATE microsub_items SET is_read = ?3 WHERE channel = ?1 AND entry_id = ?2`)
|
|
335
|
+
.bind(channel, id, flag));
|
|
336
|
+
await db.batch(statements);
|
|
337
|
+
},
|
|
338
|
+
async markReadThrough(channel, entryId) {
|
|
339
|
+
await ensureSchema();
|
|
340
|
+
const seq = await seqOf(channel, entryId);
|
|
341
|
+
if (seq === null)
|
|
342
|
+
return;
|
|
343
|
+
await db
|
|
344
|
+
.prepare(`UPDATE microsub_items SET is_read = 1 WHERE channel = ?1 AND seq <= ?2`)
|
|
345
|
+
.bind(channel, seq)
|
|
346
|
+
.run();
|
|
347
|
+
},
|
|
348
|
+
async removeItem(channel, entryId) {
|
|
349
|
+
await ensureSchema();
|
|
350
|
+
const result = await db
|
|
351
|
+
.prepare(`DELETE FROM microsub_items WHERE channel = ?1 AND entry_id = ?2`)
|
|
352
|
+
.bind(channel, entryId)
|
|
353
|
+
.run();
|
|
354
|
+
return (result.meta.changes ?? 0) > 0;
|
|
355
|
+
},
|
|
356
|
+
async searchItems(channel, query, limit) {
|
|
357
|
+
await ensureSchema();
|
|
358
|
+
const like = `%${query.replace(/[%_\\]/g, (ch) => `\\${ch}`)}%`;
|
|
359
|
+
const { results } = await db
|
|
360
|
+
.prepare(`SELECT seq, channel, entry_id, feed_url, is_read, data
|
|
361
|
+
FROM microsub_items
|
|
362
|
+
WHERE channel = ?1 AND data LIKE ?2 ESCAPE '\\'
|
|
363
|
+
ORDER BY seq DESC LIMIT ?3`)
|
|
364
|
+
.bind(channel, like, Math.max(1, limit))
|
|
365
|
+
.all();
|
|
366
|
+
return results.map(itemFromRow);
|
|
367
|
+
},
|
|
368
|
+
// Feed poll cache -----------------------------------------------------
|
|
369
|
+
async getFeedCache(feedUrl) {
|
|
370
|
+
await ensureSchema();
|
|
371
|
+
const row = await db
|
|
372
|
+
.prepare(`SELECT etag, last_modified FROM microsub_feeds WHERE feed_url = ?1`)
|
|
373
|
+
.bind(feedUrl)
|
|
374
|
+
.first();
|
|
375
|
+
if (row === null)
|
|
376
|
+
return null;
|
|
377
|
+
return { etag: row.etag, lastModified: row.last_modified };
|
|
378
|
+
},
|
|
379
|
+
async setFeedCache(feedUrl, etag, lastModified, now) {
|
|
380
|
+
await ensureSchema();
|
|
381
|
+
await db
|
|
382
|
+
.prepare(`INSERT INTO microsub_feeds (feed_url, etag, last_modified, last_polled)
|
|
383
|
+
VALUES (?1, ?2, ?3, ?4)
|
|
384
|
+
ON CONFLICT (feed_url) DO UPDATE SET
|
|
385
|
+
etag = excluded.etag,
|
|
386
|
+
last_modified = excluded.last_modified,
|
|
387
|
+
last_polled = excluded.last_polled`)
|
|
388
|
+
.bind(feedUrl, etag, lastModified, now)
|
|
389
|
+
.run();
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
package/dist/xml.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/microsub` — a minimal, dependency-free XML reader for feed parsing.
|
|
3
|
+
*
|
|
4
|
+
* Atom and RSS are XML; the Workers runtime exposes `HTMLRewriter` (an HTML
|
|
5
|
+
* tokenizer) but no XML parser, and the non-functional rules forbid pulling a
|
|
6
|
+
* heavy parser into the bundle. This module is a small recursive-descent reader
|
|
7
|
+
* tailored to feeds: elements, attributes, text, `CDATA`, comments, and the
|
|
8
|
+
* predefined/numeric entities — enough to turn a feed document into a tree the
|
|
9
|
+
* JF2 mapper walks. It is **pure** (plain string in, plain tree out) so it
|
|
10
|
+
* unit-tests without a Workers runtime.
|
|
11
|
+
*
|
|
12
|
+
* It is deliberately lenient (feeds in the wild are not always well-formed) and
|
|
13
|
+
* namespace-prefix-preserving but case-folding: element names are lowercased so
|
|
14
|
+
* `content:encoded`, `dc:creator`, and `media:content` match regardless of the
|
|
15
|
+
* source's casing.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
/** A parsed XML element: its lowercased name, attributes, and child nodes. */
|
|
20
|
+
export interface XmlElement {
|
|
21
|
+
readonly type: "element";
|
|
22
|
+
/** Lowercased tag name, prefix included (e.g. `"content:encoded"`). */
|
|
23
|
+
readonly name: string;
|
|
24
|
+
/** Lowercased-key attribute map. */
|
|
25
|
+
readonly attrs: Readonly<Record<string, string>>;
|
|
26
|
+
readonly children: readonly XmlNode[];
|
|
27
|
+
}
|
|
28
|
+
/** A run of character data (already entity-decoded). */
|
|
29
|
+
export interface XmlText {
|
|
30
|
+
readonly type: "text";
|
|
31
|
+
readonly value: string;
|
|
32
|
+
}
|
|
33
|
+
/** A node in the parsed tree. */
|
|
34
|
+
export type XmlNode = XmlElement | XmlText;
|
|
35
|
+
/** Decode XML predefined and numeric (`&#nn;` / `&#xnn;`) character references. */
|
|
36
|
+
export declare function decodeEntities(text: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Parse an XML document into a tree, returning the root element (or `null` when
|
|
39
|
+
* the input contains no element). Comments, the XML declaration, processing
|
|
40
|
+
* instructions, and `DOCTYPE` are skipped; `CDATA` sections become literal text.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseXml(input: string): XmlElement | null;
|
|
43
|
+
/** The first direct child element named `name` (case-insensitive). */
|
|
44
|
+
export declare function child(el: XmlElement, name: string): XmlElement | null;
|
|
45
|
+
/** All direct child elements named `name` (case-insensitive), in order. */
|
|
46
|
+
export declare function children(el: XmlElement, name: string): XmlElement[];
|
|
47
|
+
/** Concatenated text of an element's descendants, trimmed. */
|
|
48
|
+
export declare function text(el: XmlElement | null): string;
|
|
49
|
+
/** Text of the first child element named `name`, or `""`. */
|
|
50
|
+
export declare function childText(el: XmlElement, name: string): string;
|
|
51
|
+
//# sourceMappingURL=xml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml.d.ts","sourceRoot":"","sources":["../src/xml.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oCAAoC;IACpC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;CACvC;AAED,wDAAwD;AACxD,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,iCAAiC;AACjC,MAAM,MAAM,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC;AAU3C,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBnD;AAsBD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA+FzD;AAED,sEAAsE;AACtE,wBAAgB,KAAK,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAMrE;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAOnE;AAED,8DAA8D;AAC9D,wBAAgB,IAAI,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI,GAAG,MAAM,CAYlD;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9D"}
|
package/dist/xml.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/microsub` — a minimal, dependency-free XML reader for feed parsing.
|
|
3
|
+
*
|
|
4
|
+
* Atom and RSS are XML; the Workers runtime exposes `HTMLRewriter` (an HTML
|
|
5
|
+
* tokenizer) but no XML parser, and the non-functional rules forbid pulling a
|
|
6
|
+
* heavy parser into the bundle. This module is a small recursive-descent reader
|
|
7
|
+
* tailored to feeds: elements, attributes, text, `CDATA`, comments, and the
|
|
8
|
+
* predefined/numeric entities — enough to turn a feed document into a tree the
|
|
9
|
+
* JF2 mapper walks. It is **pure** (plain string in, plain tree out) so it
|
|
10
|
+
* unit-tests without a Workers runtime.
|
|
11
|
+
*
|
|
12
|
+
* It is deliberately lenient (feeds in the wild are not always well-formed) and
|
|
13
|
+
* namespace-prefix-preserving but case-folding: element names are lowercased so
|
|
14
|
+
* `content:encoded`, `dc:creator`, and `media:content` match regardless of the
|
|
15
|
+
* source's casing.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
const NAMED_ENTITIES = {
|
|
20
|
+
amp: "&",
|
|
21
|
+
lt: "<",
|
|
22
|
+
gt: ">",
|
|
23
|
+
quot: '"',
|
|
24
|
+
apos: "'",
|
|
25
|
+
};
|
|
26
|
+
/** Decode XML predefined and numeric (`&#nn;` / `&#xnn;`) character references. */
|
|
27
|
+
export function decodeEntities(text) {
|
|
28
|
+
return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (whole, body) => {
|
|
29
|
+
if (body[0] === "#") {
|
|
30
|
+
const code = body[1] === "x" || body[1] === "X"
|
|
31
|
+
? Number.parseInt(body.slice(2), 16)
|
|
32
|
+
: Number.parseInt(body.slice(1), 10);
|
|
33
|
+
if (!Number.isFinite(code) || code < 0 || code > 0x10ffff)
|
|
34
|
+
return whole;
|
|
35
|
+
try {
|
|
36
|
+
return String.fromCodePoint(code);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return whole;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const named = NAMED_ENTITIES[body];
|
|
43
|
+
return named ?? whole;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Parse attributes out of a start-tag's interior (everything after the name). */
|
|
47
|
+
function parseAttrs(source) {
|
|
48
|
+
const attrs = {};
|
|
49
|
+
const re = /([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g;
|
|
50
|
+
let match;
|
|
51
|
+
while ((match = re.exec(source)) !== null) {
|
|
52
|
+
const name = (match[1] ?? "").toLowerCase();
|
|
53
|
+
const raw = match[3] ?? match[4] ?? match[5] ?? "";
|
|
54
|
+
if (name !== "")
|
|
55
|
+
attrs[name] = decodeEntities(raw);
|
|
56
|
+
}
|
|
57
|
+
return attrs;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse an XML document into a tree, returning the root element (or `null` when
|
|
61
|
+
* the input contains no element). Comments, the XML declaration, processing
|
|
62
|
+
* instructions, and `DOCTYPE` are skipped; `CDATA` sections become literal text.
|
|
63
|
+
*/
|
|
64
|
+
export function parseXml(input) {
|
|
65
|
+
const root = {
|
|
66
|
+
type: "element",
|
|
67
|
+
name: "#root",
|
|
68
|
+
attrs: {},
|
|
69
|
+
children: [],
|
|
70
|
+
};
|
|
71
|
+
const stack = [root];
|
|
72
|
+
let i = 0;
|
|
73
|
+
const n = input.length;
|
|
74
|
+
const pushText = (value) => {
|
|
75
|
+
if (value === "")
|
|
76
|
+
return;
|
|
77
|
+
const parent = stack[stack.length - 1];
|
|
78
|
+
if (parent)
|
|
79
|
+
parent.children.push({ type: "text", value });
|
|
80
|
+
};
|
|
81
|
+
while (i < n) {
|
|
82
|
+
const lt = input.indexOf("<", i);
|
|
83
|
+
if (lt === -1) {
|
|
84
|
+
pushText(decodeEntities(input.slice(i)));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (lt > i) {
|
|
88
|
+
pushText(decodeEntities(input.slice(i, lt)));
|
|
89
|
+
}
|
|
90
|
+
// Comments, CDATA, DOCTYPE, and declarations all start `<!`.
|
|
91
|
+
if (input.startsWith("<!--", lt)) {
|
|
92
|
+
const end = input.indexOf("-->", lt + 4);
|
|
93
|
+
i = end === -1 ? n : end + 3;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (input.startsWith("<![CDATA[", lt)) {
|
|
97
|
+
const end = input.indexOf("]]>", lt + 9);
|
|
98
|
+
const data = input.slice(lt + 9, end === -1 ? n : end);
|
|
99
|
+
pushText(data); // CDATA is literal — no entity decoding.
|
|
100
|
+
i = end === -1 ? n : end + 3;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (input[lt + 1] === "!") {
|
|
104
|
+
// DOCTYPE or other declaration: skip to the matching `>`.
|
|
105
|
+
const end = input.indexOf(">", lt);
|
|
106
|
+
i = end === -1 ? n : end + 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (input[lt + 1] === "?") {
|
|
110
|
+
// Processing instruction / XML declaration.
|
|
111
|
+
const end = input.indexOf("?>", lt);
|
|
112
|
+
i = end === -1 ? n : end + 2;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const gt = input.indexOf(">", lt);
|
|
116
|
+
if (gt === -1) {
|
|
117
|
+
// Unterminated tag — treat the remainder as text and stop.
|
|
118
|
+
pushText(decodeEntities(input.slice(lt)));
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
let tag = input.slice(lt + 1, gt);
|
|
122
|
+
i = gt + 1;
|
|
123
|
+
if (tag[0] === "/") {
|
|
124
|
+
// Close tag: pop to the nearest matching open element.
|
|
125
|
+
const name = tag.slice(1).trim().toLowerCase();
|
|
126
|
+
for (let s = stack.length - 1; s >= 1; s--) {
|
|
127
|
+
if (stack[s]?.name === name) {
|
|
128
|
+
stack.length = s;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const selfClosing = tag.endsWith("/");
|
|
135
|
+
if (selfClosing)
|
|
136
|
+
tag = tag.slice(0, -1);
|
|
137
|
+
const space = tag.search(/\s/);
|
|
138
|
+
const name = (space === -1 ? tag : tag.slice(0, space)).toLowerCase();
|
|
139
|
+
const attrs = space === -1 ? {} : parseAttrs(tag.slice(space + 1));
|
|
140
|
+
const element = {
|
|
141
|
+
type: "element",
|
|
142
|
+
name,
|
|
143
|
+
attrs,
|
|
144
|
+
children: [],
|
|
145
|
+
};
|
|
146
|
+
const parent = stack[stack.length - 1];
|
|
147
|
+
if (parent)
|
|
148
|
+
parent.children.push(element);
|
|
149
|
+
if (!selfClosing)
|
|
150
|
+
stack.push(element);
|
|
151
|
+
}
|
|
152
|
+
const first = root.children.find((node) => node.type === "element");
|
|
153
|
+
return first ?? null;
|
|
154
|
+
}
|
|
155
|
+
/** The first direct child element named `name` (case-insensitive). */
|
|
156
|
+
export function child(el, name) {
|
|
157
|
+
const lower = name.toLowerCase();
|
|
158
|
+
for (const node of el.children) {
|
|
159
|
+
if (node.type === "element" && node.name === lower)
|
|
160
|
+
return node;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
/** All direct child elements named `name` (case-insensitive), in order. */
|
|
165
|
+
export function children(el, name) {
|
|
166
|
+
const lower = name.toLowerCase();
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const node of el.children) {
|
|
169
|
+
if (node.type === "element" && node.name === lower)
|
|
170
|
+
out.push(node);
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
/** Concatenated text of an element's descendants, trimmed. */
|
|
175
|
+
export function text(el) {
|
|
176
|
+
if (el === null)
|
|
177
|
+
return "";
|
|
178
|
+
let out = "";
|
|
179
|
+
const walk = (node) => {
|
|
180
|
+
if (node.type === "text") {
|
|
181
|
+
out += node.value;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
for (const c of node.children)
|
|
185
|
+
walk(c);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
for (const c of el.children)
|
|
189
|
+
walk(c);
|
|
190
|
+
return out.trim();
|
|
191
|
+
}
|
|
192
|
+
/** Text of the first child element named `name`, or `""`. */
|
|
193
|
+
export function childText(el, name) {
|
|
194
|
+
return text(child(el, name));
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=xml.js.map
|