With no network the home-screen app showed nothing: no service worker, and the lists the page had fetched, full text included, lived only in its memory. A service worker (web/sw/sw.ts, TypeScript checked against a worker's globals by tsconfig.sw.json, built by swc to /sw.js) goes to the network first for the page and every API read and keeps the answers, so online nothing is an old copy and #388's version check works as before; offline the kept copy answers. Scripts named by hash come from what is kept. Reads, flags and places in an episode that cannot reach the server wait in an outbox, sent in order when the connection is back (and new ones queue behind them meanwhile, so none lands out of turn); a kept list served offline shows the reads waiting there. Pictures: a worker cannot keep another site's pictures sensibly, since browsers count each opaque answer as megabytes. ipx now remembers the picture addresses in item text it sends (bounded, in memory) and /api/art fetches those as it does artwork, through the guarded client; the worker keeps them, for unread items, under a 200 MB budget, and serves them offline in place of the publisher's. Online, pictures still come from their publisher. Sign-out, or another account signing in on the device, clears what was kept. The browser suite blocks the worker except in the offline tests, since it answers before page.route can. The fixture JPEGs are placeholders a browser cannot draw; the offline test uses a real PNG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60 lines
2.9 KiB
JavaScript
60 lines
2.9 KiB
JavaScript
// Serves the fixture feeds so the daemon under test has something real to scan.
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const dir = __dirname;
|
|
const port = Number(process.env.FIXTURE_PORT || 8792);
|
|
|
|
http.createServer((req, res) => {
|
|
// Jev, for Signals (#254): sure of an item whose text has a question's keyword in it. Refuses
|
|
// a request in the shape TypeSafe refuses, as it does (#257).
|
|
if (req.method === 'POST' && req.url === '/jev') {
|
|
let body = '';
|
|
req.on('data', c => { body += c; });
|
|
req.on('end', () => {
|
|
// As TypeSafe answers a key it does not know (#271).
|
|
if (req.headers.authorization === 'Bearer bad-key') {
|
|
res.writeHead(401, { 'content-type': 'application/json' }).end('{"detail":"invalid api key"}');
|
|
return;
|
|
}
|
|
const { state, questions } = JSON.parse(body);
|
|
if (Object.values(questions).some(q => typeof q.criteria !== 'object')) {
|
|
res.writeHead(422, { 'content-type': 'application/json' }).end('{"detail":"criteria should be an object"}');
|
|
return;
|
|
}
|
|
const title = String(state).toLowerCase();
|
|
const answers = {};
|
|
for (const [id, q] of Object.entries(questions)) {
|
|
// A key's check asks about no Signal's words.
|
|
const words = /described by these words: ([^.]*)\./.exec(q.instructions)?.[1].split(', ') ?? [];
|
|
answers[id] = { noul: words.some(w => title.includes(w.toLowerCase())) ? 0.9 : 0.1 };
|
|
}
|
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ answers }));
|
|
});
|
|
return;
|
|
}
|
|
// Dated today, since a Signal looks at the last 30 days only.
|
|
if (req.url === '/signal.xml') {
|
|
const now = new Date().toUTCString();
|
|
const item = (guid, title) => `<item><guid>${guid}</guid><title>${title}</title><pubDate>${now}</pubDate></item>`;
|
|
res.writeHead(200, { 'content-type': 'application/xml' }).end(`<?xml version="1.0"?><rss version="2.0"><channel>
|
|
<title>Signal Show</title>${item('sig-1', 'Monaco qualifying')}${item('sig-2', 'Baking bread')}</channel></rss>`);
|
|
return;
|
|
}
|
|
const name = decodeURIComponent(req.url.split('?')[0].replace(/^\//, '')) || 'index';
|
|
const file = path.join(dir, path.basename(name));
|
|
fs.readFile(file, (err, body) => {
|
|
if (err) { res.writeHead(404).end('no'); return; }
|
|
const type = file.endsWith('.mp3') ? 'audio/mpeg'
|
|
: file.endsWith('.opml') ? 'text/x-opml'
|
|
// Artwork as an image, or /api/art refuses it as not one.
|
|
: file.endsWith('.jpg') ? 'image/jpeg'
|
|
// A picture a browser can draw: the JPEGs are a few bytes that only say they are one.
|
|
: file.endsWith('.png') ? 'image/png'
|
|
: file.endsWith('.html') ? 'text/html' : 'application/xml';
|
|
res.writeHead(200, { 'content-type': type, 'content-length': body.length });
|
|
res.end(body);
|
|
});
|
|
}).listen(port, '127.0.0.1', () => console.log(`fixtures on ${port}`));
|