@seedprotocol/feed-hyper 0.5.2 → 0.6.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.js CHANGED
@@ -101,12 +101,12 @@ async function T(e, t, n) {
101
101
  skip: 0
102
102
  }), h = m.length === i;
103
103
  for (let o of n.formats) {
104
- let n = u(d, o);
104
+ let n = u(d, o), c = `${r}${f(d, o)}`;
105
105
  await w(e, n, await a(m, d, o, void 0, {
106
106
  page: 1,
107
107
  pageSize: i,
108
108
  hasNext: h,
109
- baseUrl: `${r}${f(d, o)}`
109
+ baseUrl: c
110
110
  }, void 0, !1, void 0, {
111
111
  feedUrl: r,
112
112
  siteUrl: t,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/paths.ts","../src/store.ts","../src/publishFeed.ts","../src/openFeed.ts","../src/seedFeed.ts","../src/serveFeed.ts","../src/watchFeed.ts"],"sourcesContent":["import pluralize from 'pluralize'\nimport type { FeedFormat } from '@seedprotocol/feed'\n\nexport type ArchivePathOptions = {\n year: number\n month: number\n}\n\n/**\n * Map schema + format (+ optional archive) to a Hyperdrive path.\n * Layout mirrors feed HTTP routes with file extensions for Content-Type detection:\n * /posts/rss.xml\n * /posts/atom.xml\n * /posts/feed.json\n * /posts/archive/2024/2/rss.xml\n */\nexport function feedDrivePath(\n schemaName: string,\n format: FeedFormat,\n archive?: ArchivePathOptions,\n): string {\n const collection = pluralize(schemaName.toLowerCase())\n const file =\n format === 'json' ? 'feed.json' : format === 'atom' ? 'atom.xml' : 'rss.xml'\n\n if (archive) {\n return `/${collection}/archive/${archive.year}/${archive.month}/${file}`\n }\n return `/${collection}/${file}`\n}\n\n/** Registry manifest path on every published feed drive. */\nexport const REGISTRY_PATH = '/registry.json'\n\n/**\n * HTTP path segment for createFeed / channel self-links (no file extension),\n * matching historical feed.seedprotocol.io routes: /posts/rss\n */\nexport function feedHttpPath(schemaName: string, format: FeedFormat): string {\n const collection = pluralize(schemaName.toLowerCase())\n return `/${collection}/${format}`\n}\n\n/**\n * Map a gateway request pathname to the on-drive file path.\n * Accepts both historical HTTP shapes (`/posts/rss`) and drive shapes (`/posts/rss.xml`).\n * Unknown paths pass through unchanged.\n */\nexport function resolveHttpToDrivePath(pathname: string): string {\n const path = pathname.startsWith('/') ? pathname : `/${pathname}`\n const trimmed = path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path\n\n // Already a drive file path with a known feed extension\n if (/\\.(xml|json)$/i.test(trimmed)) return trimmed\n\n // /{collection}/rss|atom|json\n // /{collection}/archive/{year}/{month}/rss|atom|json\n const match = trimmed.match(\n /^(\\/[^/]+(?:\\/archive\\/\\d+\\/\\d+)?)\\/(rss|atom|json)$/i,\n )\n if (!match) return trimmed\n\n const base = match[1]!\n const format = match[2]!.toLowerCase()\n if (format === 'json') return `${base}/feed.json`\n if (format === 'atom') return `${base}/atom.xml`\n return `${base}/rss.xml`\n}\n\nexport function hyperFeedUrl(keyZ32: string): string {\n return `hyper://${keyZ32}`\n}\n","import Corestore from 'corestore'\nimport Hyperdrive from 'hyperdrive'\nimport Hyperswarm from 'hyperswarm'\nimport ID from 'hypercore-id-encoding'\n\nexport type FeedStoreHandles = {\n store: Corestore\n drive: Hyperdrive\n swarm: Hyperswarm | null\n}\n\nexport type OpenStoreOptions = {\n storePath: string\n /** Named core for a writable publisher drive (default: seed-feed). Ignored when `key` is set. */\n driveName?: string\n /** z32 or hex public key for a readonly / seeder drive */\n key?: string\n /** Join Hyperswarm and announce/lookup the drive discovery key (default: true) */\n announce?: boolean\n}\n\nfunction decodeKey(key: string): Buffer {\n return Buffer.from(ID.decode(key))\n}\n\nexport function encodeKey(key: Buffer | Uint8Array): string {\n return ID.normalize(key)\n}\n\n/**\n * Open a Corestore + Hyperdrive. With `key`, opens that drive (replicate).\n * Without `key`, opens/creates a named writable drive.\n */\nexport async function openFeedStore(options: OpenStoreOptions): Promise<FeedStoreHandles> {\n const store = new Corestore(options.storePath)\n await store.ready()\n\n const drive = options.key\n ? new Hyperdrive(store, decodeKey(options.key))\n : new Hyperdrive(store, { name: options.driveName ?? 'seed-feed' })\n\n await drive.ready()\n\n let swarm: Hyperswarm | null = null\n if (options.announce !== false) {\n swarm = new Hyperswarm()\n swarm.on('connection', (conn: unknown) => {\n store.replicate(conn as Parameters<Corestore['replicate']>[0])\n })\n swarm.join(drive.discoveryKey, { server: true, client: true })\n await swarm.flush()\n }\n\n return { store, drive, swarm }\n}\n\nexport async function closeFeedStore(handles: FeedStoreHandles): Promise<void> {\n if (handles.swarm) {\n await handles.swarm.destroy()\n }\n await handles.drive.close()\n await handles.store.close()\n}\n\n/** In-process duplex replicate between two corestores (for tests / tooling). */\nexport function replicateStores(a: Corestore, b: Corestore): () => void {\n const s1 = a.replicate(true) as NodeJS.ReadWriteStream & { destroy?: () => void }\n const s2 = b.replicate(false) as NodeJS.ReadWriteStream & { destroy?: () => void }\n s1.pipe(s2).pipe(s1)\n return () => {\n s1.destroy?.()\n s2.destroy?.()\n }\n}\n\nexport function keyToBuffer(key: string | Buffer | Uint8Array): Buffer {\n if (typeof key === 'string') return decodeKey(key)\n return Buffer.from(key)\n}\n\nexport { ID }\n","import {\n createFeed,\n getFeedItemsBySchemaName,\n type GraphQLItem,\n} from '@seedprotocol/feed'\nimport {\n REGISTRY_PATH,\n feedDrivePath,\n feedHttpPath,\n hyperFeedUrl,\n} from './paths'\nimport { closeFeedStore, encodeKey, openFeedStore, type FeedStoreHandles } from './store'\nimport type { FeedRegistry, PublishFeedOptions, PublishFeedResult } from './types'\n\nconst DEFAULT_SITE_URL = 'https://seedprotocol.io'\nconst DEFAULT_FEED_TITLE = 'Seed Protocol'\n\nasync function resolveSiteDefaults(overrides?: {\n siteUrl?: string\n title?: string\n}): Promise<{ siteUrl: string; title: string }> {\n try {\n const feed = await import('@seedprotocol/feed')\n const cfg =\n typeof feed.getSiteConfig === 'function' ? feed.getSiteConfig() : null\n return {\n siteUrl: overrides?.siteUrl ?? cfg?.siteUrl ?? DEFAULT_SITE_URL,\n title: overrides?.title ?? cfg?.title ?? DEFAULT_FEED_TITLE,\n }\n } catch {\n return {\n siteUrl: overrides?.siteUrl ?? DEFAULT_SITE_URL,\n title: overrides?.title ?? DEFAULT_FEED_TITLE,\n }\n }\n}\n\nasync function putUtf8(\n drive: { put: (path: string, buf: Buffer) => Promise<unknown> },\n path: string,\n content: string,\n): Promise<void> {\n await drive.put(path, Buffer.from(content, 'utf-8'))\n}\n\nasync function writeFeedsToDrive(\n drive: FeedStoreHandles['drive'],\n key: string,\n options: PublishFeedOptions,\n): Promise<{ paths: string[]; hyperUrl: string }> {\n const hyperUrl = hyperFeedUrl(key)\n const pageSize = options.pageSize ?? 25\n const paths: string[] = []\n const registrySchemas: FeedRegistry['schemas'] = []\n\n if (options.fixtureContents) {\n for (const [path, content] of Object.entries(options.fixtureContents)) {\n await putUtf8(drive, path, content)\n paths.push(path)\n }\n registrySchemas.push({\n schema: options.schemas[0]!,\n formats: options.formats,\n paths: [...paths],\n })\n } else {\n const { siteUrl, title } = await resolveSiteDefaults({\n siteUrl: options.siteUrl,\n })\n for (const schema of options.schemas) {\n const schemaPaths: string[] = []\n const items = (await getFeedItemsBySchemaName(schema, {\n limit: pageSize,\n skip: 0,\n })) as GraphQLItem[]\n const hasNext = items.length === pageSize\n\n for (const format of options.formats) {\n const drivePath = feedDrivePath(schema, format)\n const baseUrl = `${hyperUrl}${feedHttpPath(schema, format)}`\n const content = await createFeed(\n items,\n schema,\n format,\n undefined,\n {\n page: 1,\n pageSize,\n hasNext,\n baseUrl,\n },\n undefined,\n false,\n undefined,\n {\n feedUrl: hyperUrl,\n siteUrl,\n title,\n },\n )\n await putUtf8(drive, drivePath, content)\n paths.push(drivePath)\n schemaPaths.push(drivePath)\n }\n\n if (options.includeArchives) {\n const now = new Date()\n const year = now.getFullYear()\n const month = now.getMonth() + 1\n for (const format of options.formats) {\n const archivePath = feedDrivePath(schema, format, { year, month })\n const archiveContent = await createFeed(\n items,\n schema,\n format,\n undefined,\n undefined,\n [\n {\n rel: 'current',\n href: `${hyperUrl}${feedHttpPath(schema, format)}`,\n },\n ],\n true,\n undefined,\n { feedUrl: hyperUrl, siteUrl },\n )\n await putUtf8(drive, archivePath, archiveContent)\n paths.push(archivePath)\n schemaPaths.push(archivePath)\n }\n }\n\n registrySchemas.push({\n schema,\n formats: [...options.formats],\n paths: schemaPaths,\n })\n }\n }\n\n const registry: FeedRegistry = {\n key,\n version: drive.version,\n updatedAt: new Date().toISOString(),\n schemas: registrySchemas,\n }\n await putUtf8(drive, REGISTRY_PATH, JSON.stringify(registry, null, 2))\n paths.push(REGISTRY_PATH)\n\n return { paths, hyperUrl }\n}\n\nexport type PublishFeedSession = PublishFeedResult & {\n /** Close Corestore / Hyperdrive / Hyperswarm. No-op if already closed. */\n close: () => Promise<void>\n}\n\n/**\n * Generate feeds via `@seedprotocol/feed` and write them into a Hyperdrive.\n *\n * - `announce: false` — write, close store, return (one-shot).\n * - `announce: true` (default) — write, keep swarm joined until `close()`.\n */\nexport async function publishFeed(\n options: PublishFeedOptions,\n): Promise<PublishFeedSession> {\n if (!options.schemas.length) {\n throw new Error('publishFeed: schemas must be non-empty')\n }\n if (!options.formats.length) {\n throw new Error('publishFeed: formats must be non-empty')\n }\n\n const announce = options.announce !== false\n const handles = await openFeedStore({\n storePath: options.storePath,\n driveName: options.driveName,\n announce,\n })\n\n const key = encodeKey(handles.drive.key)\n const { paths, hyperUrl } = await writeFeedsToDrive(handles.drive, key, options)\n const version = handles.drive.version\n\n let closed = false\n const close = async () => {\n if (closed) return\n closed = true\n await closeFeedStore(handles)\n }\n\n if (!announce) {\n await close()\n }\n\n return {\n key,\n version,\n paths,\n hyperUrl,\n close,\n }\n}\n","import { REGISTRY_PATH } from './paths'\nimport { closeFeedStore, encodeKey, openFeedStore, type FeedStoreHandles } from './store'\nimport type { FeedRegistry, OpenFeedOptions } from './types'\n\nexport type OpenedFeed = {\n key: string\n version: number\n drive: FeedStoreHandles['drive']\n get: (path: string) => Promise<string | null>\n getRegistry: () => Promise<FeedRegistry | null>\n close: () => Promise<void>\n}\n\n/**\n * Open a feed Hyperdrive by public key, join the swarm, and optionally wait for data.\n */\nexport async function openFeed(options: OpenFeedOptions): Promise<OpenedFeed> {\n const handles = await openFeedStore({\n storePath: options.storePath,\n key: options.key,\n announce: options.announce !== false,\n })\n\n const syncTimeoutMs = options.syncTimeoutMs ?? 15_000\n if (handles.drive.core.length === 0 && syncTimeoutMs > 0) {\n await Promise.race([\n handles.drive.core.update({ wait: true }),\n new Promise<void>((resolve) => setTimeout(resolve, syncTimeoutMs)),\n ]).catch(() => {\n /* timeout or update failure — caller may still read if peers arrive later */\n })\n }\n\n const get = async (path: string): Promise<string | null> => {\n const buf = await handles.drive.get(path)\n if (!buf) return null\n return Buffer.from(buf).toString('utf-8')\n }\n\n const getRegistry = async (): Promise<FeedRegistry | null> => {\n const raw = await get(REGISTRY_PATH)\n if (!raw) return null\n try {\n return JSON.parse(raw) as FeedRegistry\n } catch {\n return null\n }\n }\n\n return {\n key: encodeKey(handles.drive.key),\n version: handles.drive.version,\n drive: handles.drive,\n get,\n getRegistry,\n close: () => closeFeedStore(handles),\n }\n}\n","import { closeFeedStore, encodeKey, openFeedStore } from './store'\nimport type { SeedFeedOptions } from './types'\n\nexport type SeedFeedSession = {\n key: string\n close: () => Promise<void>\n}\n\n/**\n * Join the swarm and replicate a feed drive until `close()` (or process exit).\n * Does not generate feed content.\n */\nexport async function seedFeed(options: SeedFeedOptions): Promise<SeedFeedSession> {\n const handles = await openFeedStore({\n storePath: options.storePath,\n key: options.key,\n announce: true,\n })\n\n // Prefer longer cores from peers\n try {\n await handles.drive.core.update({ wait: false })\n } catch {\n /* ignore */\n }\n\n return {\n key: encodeKey(handles.drive.key),\n close: () => closeFeedStore(handles),\n }\n}\n","import http from 'node:http'\nimport ServeDrive from 'serve-drive'\nimport { feedDrivePath, resolveHttpToDrivePath } from './paths'\nimport { closeFeedStore, encodeKey, openFeedStore } from './store'\nimport type { ServeFeedOptions, ServeFeedResult } from './types'\n\n/**\n * Rewrite extensionless feed URLs (`/posts/rss`) to drive paths (`/posts/rss.xml`)\n * so serve-drive looks up the real file and gets the correct Content-Type.\n */\nfunction attachFeedPathRewrite(server: http.Server): void {\n server.on('request', (req) => {\n if (!req.url) return\n const q = req.url.indexOf('?')\n const pathname = q === -1 ? req.url : req.url.slice(0, q)\n const query = q === -1 ? '' : req.url.slice(q)\n const resolved = resolveHttpToDrivePath(pathname)\n if (resolved !== pathname) {\n req.url = resolved + query\n }\n })\n}\n\n/**\n * Seed a feed drive and expose it over localhost HTTP via serve-drive.\n * Serves both drive paths (`/posts/rss.xml`) and historical HTTP paths (`/posts/rss`).\n */\nexport async function serveFeed(options: ServeFeedOptions): Promise<ServeFeedResult> {\n const host = options.host ?? '127.0.0.1'\n const port = options.port ?? 8080\n\n const handles = await openFeedStore({\n storePath: options.storePath,\n key: options.key,\n announce: options.announce !== false,\n })\n\n try {\n await handles.drive.core.update({ wait: false })\n } catch {\n /* ignore */\n }\n\n const key = encodeKey(handles.drive.key)\n\n const server = http.createServer()\n // Must register before ServeDrive attaches its handler so rewrite runs first.\n attachFeedPathRewrite(server)\n\n const serve = new ServeDrive({\n server,\n port,\n host,\n anyPort: port === 0,\n // Local RSS gateways need unauthenticated GETs; enable token for public hosts via env later\n token: false,\n get: async ({ key: requestKey }: { key: Buffer | null; filename: string; version: number }) => {\n if (requestKey && !requestKey.equals(handles.drive.key)) {\n return null\n }\n return handles.drive\n },\n })\n\n await serve.ready()\n\n const boundPort =\n typeof serve.address === 'function'\n ? (serve.address()?.port ?? port)\n : port\n\n const baseUrl = `http://${host}:${boundPort}`\n\n return {\n key,\n port: boundPort,\n host,\n baseUrl,\n close: async () => {\n if (typeof serve.close === 'function') {\n await serve.close()\n } else if (typeof (serve as { suspend?: () => Promise<void> }).suspend === 'function') {\n await (serve as { suspend: () => Promise<void> }).suspend()\n }\n await closeFeedStore(handles)\n },\n }\n}\n\n/** Example local URL for a schema/format after serveFeed (drive path with extension). */\nexport function localFeedUrl(\n baseUrl: string,\n schemaName: string,\n format: 'rss' | 'atom' | 'json',\n): string {\n return `${baseUrl.replace(/\\/$/, '')}${feedDrivePath(schemaName, format)}`\n}\n","import type Hyperdrive from 'hyperdrive'\n\nexport type WatchFeedOptions = {\n /** Called whenever the drive version increases. */\n onUpdate: (version: number) => void | Promise<void>\n}\n\n/**\n * Watch a Hyperdrive for new versions (append-only length growth).\n * Returns an unsubscribe function.\n */\nexport function watchFeed(\n drive: Hyperdrive,\n options: WatchFeedOptions,\n): () => void {\n const core = drive.core\n let stopped = false\n\n const onAppend = () => {\n if (stopped) return\n void Promise.resolve(options.onUpdate(drive.version)).catch((err) => {\n console.error('[feed-hyper] watchFeed onUpdate error:', err)\n })\n }\n\n core.on('append', onAppend)\n\n return () => {\n stopped = true\n core.off('append', onAppend)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/paths.ts","../src/store.ts","../src/publishFeed.ts","../src/openFeed.ts","../src/seedFeed.ts","../src/serveFeed.ts","../src/watchFeed.ts"],"sourcesContent":["import pluralize from 'pluralize'\nimport type { FeedFormat } from '@seedprotocol/feed'\n\nexport type ArchivePathOptions = {\n year: number\n month: number\n}\n\n/**\n * Map schema + format (+ optional archive) to a Hyperdrive path.\n * Layout mirrors feed HTTP routes with file extensions for Content-Type detection:\n * /posts/rss.xml\n * /posts/atom.xml\n * /posts/feed.json\n * /posts/archive/2024/2/rss.xml\n */\nexport function feedDrivePath(\n schemaName: string,\n format: FeedFormat,\n archive?: ArchivePathOptions,\n): string {\n const collection = pluralize(schemaName.toLowerCase())\n const file =\n format === 'json' ? 'feed.json' : format === 'atom' ? 'atom.xml' : 'rss.xml'\n\n if (archive) {\n return `/${collection}/archive/${archive.year}/${archive.month}/${file}`\n }\n return `/${collection}/${file}`\n}\n\n/** Registry manifest path on every published feed drive. */\nexport const REGISTRY_PATH = '/registry.json'\n\n/**\n * HTTP path segment for createFeed / channel self-links (no file extension),\n * matching historical feed.seedprotocol.io routes: /posts/rss\n */\nexport function feedHttpPath(schemaName: string, format: FeedFormat): string {\n const collection = pluralize(schemaName.toLowerCase())\n return `/${collection}/${format}`\n}\n\n/**\n * Map a gateway request pathname to the on-drive file path.\n * Accepts both historical HTTP shapes (`/posts/rss`) and drive shapes (`/posts/rss.xml`).\n * Unknown paths pass through unchanged.\n */\nexport function resolveHttpToDrivePath(pathname: string): string {\n const path = pathname.startsWith('/') ? pathname : `/${pathname}`\n const trimmed = path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path\n\n // Already a drive file path with a known feed extension\n if (/\\.(xml|json)$/i.test(trimmed)) return trimmed\n\n // /{collection}/rss|atom|json\n // /{collection}/archive/{year}/{month}/rss|atom|json\n const match = trimmed.match(\n /^(\\/[^/]+(?:\\/archive\\/\\d+\\/\\d+)?)\\/(rss|atom|json)$/i,\n )\n if (!match) return trimmed\n\n const base = match[1]!\n const format = match[2]!.toLowerCase()\n if (format === 'json') return `${base}/feed.json`\n if (format === 'atom') return `${base}/atom.xml`\n return `${base}/rss.xml`\n}\n\nexport function hyperFeedUrl(keyZ32: string): string {\n return `hyper://${keyZ32}`\n}\n","import Corestore from 'corestore'\nimport Hyperdrive from 'hyperdrive'\nimport Hyperswarm from 'hyperswarm'\nimport ID from 'hypercore-id-encoding'\n\nexport type FeedStoreHandles = {\n store: Corestore\n drive: Hyperdrive\n swarm: Hyperswarm | null\n}\n\nexport type OpenStoreOptions = {\n storePath: string\n /** Named core for a writable publisher drive (default: seed-feed). Ignored when `key` is set. */\n driveName?: string\n /** z32 or hex public key for a readonly / seeder drive */\n key?: string\n /** Join Hyperswarm and announce/lookup the drive discovery key (default: true) */\n announce?: boolean\n}\n\nfunction decodeKey(key: string): Buffer {\n return Buffer.from(ID.decode(key))\n}\n\nexport function encodeKey(key: Buffer | Uint8Array): string {\n return ID.normalize(key)\n}\n\n/**\n * Open a Corestore + Hyperdrive. With `key`, opens that drive (replicate).\n * Without `key`, opens/creates a named writable drive.\n */\nexport async function openFeedStore(options: OpenStoreOptions): Promise<FeedStoreHandles> {\n const store = new Corestore(options.storePath)\n await store.ready()\n\n const drive = options.key\n ? new Hyperdrive(store, decodeKey(options.key))\n : new Hyperdrive(store, { name: options.driveName ?? 'seed-feed' })\n\n await drive.ready()\n\n let swarm: Hyperswarm | null = null\n if (options.announce !== false) {\n swarm = new Hyperswarm()\n swarm.on('connection', (conn: unknown) => {\n store.replicate(conn as Parameters<Corestore['replicate']>[0])\n })\n swarm.join(drive.discoveryKey, { server: true, client: true })\n await swarm.flush()\n }\n\n return { store, drive, swarm }\n}\n\nexport async function closeFeedStore(handles: FeedStoreHandles): Promise<void> {\n if (handles.swarm) {\n await handles.swarm.destroy()\n }\n await handles.drive.close()\n await handles.store.close()\n}\n\n/** In-process duplex replicate between two corestores (for tests / tooling). */\nexport function replicateStores(a: Corestore, b: Corestore): () => void {\n const s1 = a.replicate(true) as NodeJS.ReadWriteStream & { destroy?: () => void }\n const s2 = b.replicate(false) as NodeJS.ReadWriteStream & { destroy?: () => void }\n s1.pipe(s2).pipe(s1)\n return () => {\n s1.destroy?.()\n s2.destroy?.()\n }\n}\n\nexport function keyToBuffer(key: string | Buffer | Uint8Array): Buffer {\n if (typeof key === 'string') return decodeKey(key)\n return Buffer.from(key)\n}\n\nexport { ID }\n","import {\n createFeed,\n getFeedItemsBySchemaName,\n type GraphQLItem,\n} from '@seedprotocol/feed'\nimport {\n REGISTRY_PATH,\n feedDrivePath,\n feedHttpPath,\n hyperFeedUrl,\n} from './paths'\nimport { closeFeedStore, encodeKey, openFeedStore, type FeedStoreHandles } from './store'\nimport type { FeedRegistry, PublishFeedOptions, PublishFeedResult } from './types'\n\nconst DEFAULT_SITE_URL = 'https://seedprotocol.io'\nconst DEFAULT_FEED_TITLE = 'Seed Protocol'\n\nasync function resolveSiteDefaults(overrides?: {\n siteUrl?: string\n title?: string\n}): Promise<{ siteUrl: string; title: string }> {\n try {\n const feed = await import('@seedprotocol/feed')\n const cfg =\n typeof feed.getSiteConfig === 'function' ? feed.getSiteConfig() : null\n return {\n siteUrl: overrides?.siteUrl ?? cfg?.siteUrl ?? DEFAULT_SITE_URL,\n title: overrides?.title ?? cfg?.title ?? DEFAULT_FEED_TITLE,\n }\n } catch {\n return {\n siteUrl: overrides?.siteUrl ?? DEFAULT_SITE_URL,\n title: overrides?.title ?? DEFAULT_FEED_TITLE,\n }\n }\n}\n\nasync function putUtf8(\n drive: { put: (path: string, buf: Buffer) => Promise<unknown> },\n path: string,\n content: string,\n): Promise<void> {\n await drive.put(path, Buffer.from(content, 'utf-8'))\n}\n\nasync function writeFeedsToDrive(\n drive: FeedStoreHandles['drive'],\n key: string,\n options: PublishFeedOptions,\n): Promise<{ paths: string[]; hyperUrl: string }> {\n const hyperUrl = hyperFeedUrl(key)\n const pageSize = options.pageSize ?? 25\n const paths: string[] = []\n const registrySchemas: FeedRegistry['schemas'] = []\n\n if (options.fixtureContents) {\n for (const [path, content] of Object.entries(options.fixtureContents)) {\n await putUtf8(drive, path, content)\n paths.push(path)\n }\n registrySchemas.push({\n schema: options.schemas[0]!,\n formats: options.formats,\n paths: [...paths],\n })\n } else {\n const { siteUrl, title } = await resolveSiteDefaults({\n siteUrl: options.siteUrl,\n })\n for (const schema of options.schemas) {\n const schemaPaths: string[] = []\n const items = (await getFeedItemsBySchemaName(schema, {\n limit: pageSize,\n skip: 0,\n })) as GraphQLItem[]\n const hasNext = items.length === pageSize\n\n for (const format of options.formats) {\n const drivePath = feedDrivePath(schema, format)\n const baseUrl = `${hyperUrl}${feedHttpPath(schema, format)}`\n const content = await createFeed(\n items,\n schema,\n format,\n undefined,\n {\n page: 1,\n pageSize,\n hasNext,\n baseUrl,\n },\n undefined,\n false,\n undefined,\n {\n feedUrl: hyperUrl,\n siteUrl,\n title,\n },\n )\n await putUtf8(drive, drivePath, content)\n paths.push(drivePath)\n schemaPaths.push(drivePath)\n }\n\n if (options.includeArchives) {\n const now = new Date()\n const year = now.getFullYear()\n const month = now.getMonth() + 1\n for (const format of options.formats) {\n const archivePath = feedDrivePath(schema, format, { year, month })\n const archiveContent = await createFeed(\n items,\n schema,\n format,\n undefined,\n undefined,\n [\n {\n rel: 'current',\n href: `${hyperUrl}${feedHttpPath(schema, format)}`,\n },\n ],\n true,\n undefined,\n { feedUrl: hyperUrl, siteUrl },\n )\n await putUtf8(drive, archivePath, archiveContent)\n paths.push(archivePath)\n schemaPaths.push(archivePath)\n }\n }\n\n registrySchemas.push({\n schema,\n formats: [...options.formats],\n paths: schemaPaths,\n })\n }\n }\n\n const registry: FeedRegistry = {\n key,\n version: drive.version,\n updatedAt: new Date().toISOString(),\n schemas: registrySchemas,\n }\n await putUtf8(drive, REGISTRY_PATH, JSON.stringify(registry, null, 2))\n paths.push(REGISTRY_PATH)\n\n return { paths, hyperUrl }\n}\n\nexport type PublishFeedSession = PublishFeedResult & {\n /** Close Corestore / Hyperdrive / Hyperswarm. No-op if already closed. */\n close: () => Promise<void>\n}\n\n/**\n * Generate feeds via `@seedprotocol/feed` and write them into a Hyperdrive.\n *\n * - `announce: false` — write, close store, return (one-shot).\n * - `announce: true` (default) — write, keep swarm joined until `close()`.\n */\nexport async function publishFeed(\n options: PublishFeedOptions,\n): Promise<PublishFeedSession> {\n if (!options.schemas.length) {\n throw new Error('publishFeed: schemas must be non-empty')\n }\n if (!options.formats.length) {\n throw new Error('publishFeed: formats must be non-empty')\n }\n\n const announce = options.announce !== false\n const handles = await openFeedStore({\n storePath: options.storePath,\n driveName: options.driveName,\n announce,\n })\n\n const key = encodeKey(handles.drive.key)\n const { paths, hyperUrl } = await writeFeedsToDrive(handles.drive, key, options)\n const version = handles.drive.version\n\n let closed = false\n const close = async () => {\n if (closed) return\n closed = true\n await closeFeedStore(handles)\n }\n\n if (!announce) {\n await close()\n }\n\n return {\n key,\n version,\n paths,\n hyperUrl,\n close,\n }\n}\n","import { REGISTRY_PATH } from './paths'\nimport { closeFeedStore, encodeKey, openFeedStore, type FeedStoreHandles } from './store'\nimport type { FeedRegistry, OpenFeedOptions } from './types'\n\nexport type OpenedFeed = {\n key: string\n version: number\n drive: FeedStoreHandles['drive']\n get: (path: string) => Promise<string | null>\n getRegistry: () => Promise<FeedRegistry | null>\n close: () => Promise<void>\n}\n\n/**\n * Open a feed Hyperdrive by public key, join the swarm, and optionally wait for data.\n */\nexport async function openFeed(options: OpenFeedOptions): Promise<OpenedFeed> {\n const handles = await openFeedStore({\n storePath: options.storePath,\n key: options.key,\n announce: options.announce !== false,\n })\n\n const syncTimeoutMs = options.syncTimeoutMs ?? 15_000\n if (handles.drive.core.length === 0 && syncTimeoutMs > 0) {\n await Promise.race([\n handles.drive.core.update({ wait: true }),\n new Promise<void>((resolve) => setTimeout(resolve, syncTimeoutMs)),\n ]).catch(() => {\n /* timeout or update failure — caller may still read if peers arrive later */\n })\n }\n\n const get = async (path: string): Promise<string | null> => {\n const buf = await handles.drive.get(path)\n if (!buf) return null\n return Buffer.from(buf).toString('utf-8')\n }\n\n const getRegistry = async (): Promise<FeedRegistry | null> => {\n const raw = await get(REGISTRY_PATH)\n if (!raw) return null\n try {\n return JSON.parse(raw) as FeedRegistry\n } catch {\n return null\n }\n }\n\n return {\n key: encodeKey(handles.drive.key),\n version: handles.drive.version,\n drive: handles.drive,\n get,\n getRegistry,\n close: () => closeFeedStore(handles),\n }\n}\n","import { closeFeedStore, encodeKey, openFeedStore } from './store'\nimport type { SeedFeedOptions } from './types'\n\nexport type SeedFeedSession = {\n key: string\n close: () => Promise<void>\n}\n\n/**\n * Join the swarm and replicate a feed drive until `close()` (or process exit).\n * Does not generate feed content.\n */\nexport async function seedFeed(options: SeedFeedOptions): Promise<SeedFeedSession> {\n const handles = await openFeedStore({\n storePath: options.storePath,\n key: options.key,\n announce: true,\n })\n\n // Prefer longer cores from peers\n try {\n await handles.drive.core.update({ wait: false })\n } catch {\n /* ignore */\n }\n\n return {\n key: encodeKey(handles.drive.key),\n close: () => closeFeedStore(handles),\n }\n}\n","import http from 'node:http'\nimport ServeDrive from 'serve-drive'\nimport { feedDrivePath, resolveHttpToDrivePath } from './paths'\nimport { closeFeedStore, encodeKey, openFeedStore } from './store'\nimport type { ServeFeedOptions, ServeFeedResult } from './types'\n\n/**\n * Rewrite extensionless feed URLs (`/posts/rss`) to drive paths (`/posts/rss.xml`)\n * so serve-drive looks up the real file and gets the correct Content-Type.\n */\nfunction attachFeedPathRewrite(server: http.Server): void {\n server.on('request', (req) => {\n if (!req.url) return\n const q = req.url.indexOf('?')\n const pathname = q === -1 ? req.url : req.url.slice(0, q)\n const query = q === -1 ? '' : req.url.slice(q)\n const resolved = resolveHttpToDrivePath(pathname)\n if (resolved !== pathname) {\n req.url = resolved + query\n }\n })\n}\n\n/**\n * Seed a feed drive and expose it over localhost HTTP via serve-drive.\n * Serves both drive paths (`/posts/rss.xml`) and historical HTTP paths (`/posts/rss`).\n */\nexport async function serveFeed(options: ServeFeedOptions): Promise<ServeFeedResult> {\n const host = options.host ?? '127.0.0.1'\n const port = options.port ?? 8080\n\n const handles = await openFeedStore({\n storePath: options.storePath,\n key: options.key,\n announce: options.announce !== false,\n })\n\n try {\n await handles.drive.core.update({ wait: false })\n } catch {\n /* ignore */\n }\n\n const key = encodeKey(handles.drive.key)\n\n const server = http.createServer()\n // Must register before ServeDrive attaches its handler so rewrite runs first.\n attachFeedPathRewrite(server)\n\n const serve = new ServeDrive({\n server,\n port,\n host,\n anyPort: port === 0,\n // Local RSS gateways need unauthenticated GETs; enable token for public hosts via env later\n token: false,\n get: async ({ key: requestKey }: { key: Buffer | null; filename: string; version: number }) => {\n if (requestKey && !requestKey.equals(handles.drive.key)) {\n return null\n }\n return handles.drive\n },\n })\n\n await serve.ready()\n\n const boundPort =\n typeof serve.address === 'function'\n ? (serve.address()?.port ?? port)\n : port\n\n const baseUrl = `http://${host}:${boundPort}`\n\n return {\n key,\n port: boundPort,\n host,\n baseUrl,\n close: async () => {\n if (typeof serve.close === 'function') {\n await serve.close()\n } else if (typeof (serve as { suspend?: () => Promise<void> }).suspend === 'function') {\n await (serve as { suspend: () => Promise<void> }).suspend()\n }\n await closeFeedStore(handles)\n },\n }\n}\n\n/** Example local URL for a schema/format after serveFeed (drive path with extension). */\nexport function localFeedUrl(\n baseUrl: string,\n schemaName: string,\n format: 'rss' | 'atom' | 'json',\n): string {\n return `${baseUrl.replace(/\\/$/, '')}${feedDrivePath(schemaName, format)}`\n}\n","import type Hyperdrive from 'hyperdrive'\n\nexport type WatchFeedOptions = {\n /** Called whenever the drive version increases. */\n onUpdate: (version: number) => void | Promise<void>\n}\n\n/**\n * Watch a Hyperdrive for new versions (append-only length growth).\n * Returns an unsubscribe function.\n */\nexport function watchFeed(\n drive: Hyperdrive,\n options: WatchFeedOptions,\n): () => void {\n const core = drive.core\n let stopped = false\n\n const onAppend = () => {\n if (stopped) return\n void Promise.resolve(options.onUpdate(drive.version)).catch((err) => {\n console.error('[feed-hyper] watchFeed onUpdate error:', err)\n })\n }\n\n core.on('append', onAppend)\n\n return () => {\n stopped = true\n core.off('append', onAppend)\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seedprotocol/feed-hyper",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Hyperdrive / Hyperswarm distribution for Seed Protocol feeds",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,8 +29,8 @@
29
29
  "prepublishOnly": "bun run build && node ../../scripts/sync-versions.js"
30
30
  },
31
31
  "dependencies": {
32
- "@seedprotocol/arweave": "0.5.2",
33
- "@seedprotocol/feed": "0.5.2",
32
+ "@seedprotocol/arweave": "0.6.0",
33
+ "@seedprotocol/feed": "0.6.0",
34
34
  "corestore": "^7.12.0",
35
35
  "hypercore-id-encoding": "^1.3.0",
36
36
  "hyperdrive": "^13.0.0",