@dxos/plugin-magazine 0.0.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 +105 -0
- package/PLUGIN.mdl +653 -0
- package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs +90 -0
- package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs.map +7 -0
- package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs +97 -0
- package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs.map +7 -0
- package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs +325 -0
- package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs.map +7 -0
- package/dist/lib/neutral/MagazinePlugin.mjs +62 -0
- package/dist/lib/neutral/MagazinePlugin.mjs.map +7 -0
- package/dist/lib/neutral/MagazinePlugin.workerd.mjs +12 -0
- package/dist/lib/neutral/MagazinePlugin.workerd.mjs.map +7 -0
- package/dist/lib/neutral/PostArticle-RERHXPXC.mjs +230 -0
- package/dist/lib/neutral/PostArticle-RERHXPXC.mjs.map +7 -0
- package/dist/lib/neutral/PostCard-VDGN7BBA.mjs +60 -0
- package/dist/lib/neutral/PostCard-VDGN7BBA.mjs.map +7 -0
- package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs +121 -0
- package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs.map +7 -0
- package/dist/lib/neutral/capabilities/index.mjs +21 -0
- package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
- package/dist/lib/neutral/chunk-3K4AQKBZ.mjs +12 -0
- package/dist/lib/neutral/chunk-3K4AQKBZ.mjs.map +7 -0
- package/dist/lib/neutral/chunk-AFPBWBTK.mjs +591 -0
- package/dist/lib/neutral/chunk-AFPBWBTK.mjs.map +7 -0
- package/dist/lib/neutral/chunk-APQRTYT3.mjs +57 -0
- package/dist/lib/neutral/chunk-APQRTYT3.mjs.map +7 -0
- package/dist/lib/neutral/chunk-GU725ZF4.mjs +11 -0
- package/dist/lib/neutral/chunk-GU725ZF4.mjs.map +7 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/neutral/chunk-JFPR6OV4.mjs +60 -0
- package/dist/lib/neutral/chunk-JFPR6OV4.mjs.map +7 -0
- package/dist/lib/neutral/chunk-KTYLUOUG.mjs +36 -0
- package/dist/lib/neutral/chunk-KTYLUOUG.mjs.map +7 -0
- package/dist/lib/neutral/chunk-MBPX7RPI.mjs +14 -0
- package/dist/lib/neutral/chunk-MBPX7RPI.mjs.map +7 -0
- package/dist/lib/neutral/chunk-MJ2AY2CK.mjs +8 -0
- package/dist/lib/neutral/chunk-MJ2AY2CK.mjs.map +7 -0
- package/dist/lib/neutral/chunk-OYCDRSWB.mjs +624 -0
- package/dist/lib/neutral/chunk-OYCDRSWB.mjs.map +7 -0
- package/dist/lib/neutral/chunk-SAWKLGJU.mjs +28 -0
- package/dist/lib/neutral/chunk-SAWKLGJU.mjs.map +7 -0
- package/dist/lib/neutral/chunk-XCNCK47N.mjs +15 -0
- package/dist/lib/neutral/chunk-XCNCK47N.mjs.map +7 -0
- package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs +59 -0
- package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs.map +7 -0
- package/dist/lib/neutral/components/index.mjs +338 -0
- package/dist/lib/neutral/components/index.mjs.map +7 -0
- package/dist/lib/neutral/containers/index.mjs +17 -0
- package/dist/lib/neutral/containers/index.mjs.map +7 -0
- package/dist/lib/neutral/create-object-SYZMRR4H.mjs +98 -0
- package/dist/lib/neutral/create-object-SYZMRR4H.mjs.map +7 -0
- package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs +184 -0
- package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs.map +7 -0
- package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs +31 -0
- package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +18 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs +65 -0
- package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/neutral/meta.mjs +8 -0
- package/dist/lib/neutral/meta.mjs.map +7 -0
- package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs +18 -0
- package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs.map +7 -0
- package/dist/lib/neutral/operation-handler-JE3743BU.mjs +8 -0
- package/dist/lib/neutral/operation-handler-JE3743BU.mjs.map +7 -0
- package/dist/lib/neutral/operations/index.mjs +8 -0
- package/dist/lib/neutral/operations/index.mjs.map +7 -0
- package/dist/lib/neutral/plugin.mjs +16 -0
- package/dist/lib/neutral/plugin.mjs.map +7 -0
- package/dist/lib/neutral/plugin.workerd.mjs +16 -0
- package/dist/lib/neutral/plugin.workerd.mjs.map +7 -0
- package/dist/lib/neutral/react-surface-TZESAMG2.mjs +60 -0
- package/dist/lib/neutral/react-surface-TZESAMG2.mjs.map +7 -0
- package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs +52 -0
- package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs.map +7 -0
- package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs +8 -0
- package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs.map +7 -0
- package/dist/lib/neutral/skills/index.mjs +46 -0
- package/dist/lib/neutral/skills/index.mjs.map +7 -0
- package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs +99 -0
- package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs.map +7 -0
- package/dist/lib/neutral/translations.mjs +81 -0
- package/dist/lib/neutral/translations.mjs.map +7 -0
- package/dist/lib/neutral/types/index.mjs +14 -0
- package/dist/lib/neutral/types/index.mjs.map +7 -0
- package/dist/types/dx.config.d.ts +28 -0
- package/dist/types/dx.config.d.ts.map +1 -0
- package/dist/types/src/MagazinePlugin.d.ts +4 -0
- package/dist/types/src/MagazinePlugin.d.ts.map +1 -0
- package/dist/types/src/MagazinePlugin.test.d.ts +2 -0
- package/dist/types/src/MagazinePlugin.test.d.ts.map +1 -0
- package/dist/types/src/MagazinePlugin.workerd.d.ts +4 -0
- package/dist/types/src/MagazinePlugin.workerd.d.ts.map +1 -0
- package/dist/types/src/atoms/atoms.test.d.ts +2 -0
- package/dist/types/src/atoms/atoms.test.d.ts.map +1 -0
- package/dist/types/src/atoms/index.d.ts +7 -0
- package/dist/types/src/atoms/index.d.ts.map +1 -0
- package/dist/types/src/atoms/magazine-posts.d.ts +16 -0
- package/dist/types/src/atoms/magazine-posts.d.ts.map +1 -0
- package/dist/types/src/atoms/post-content.d.ts +7 -0
- package/dist/types/src/atoms/post-content.d.ts.map +1 -0
- package/dist/types/src/atoms/post-curation.d.ts +16 -0
- package/dist/types/src/atoms/post-curation.d.ts.map +1 -0
- package/dist/types/src/atoms/post-display.d.ts +22 -0
- package/dist/types/src/atoms/post-display.d.ts.map +1 -0
- package/dist/types/src/atoms/post-read.d.ts +14 -0
- package/dist/types/src/atoms/post-read.d.ts.map +1 -0
- package/dist/types/src/atoms/post-tags.d.ts +16 -0
- package/dist/types/src/atoms/post-tags.d.ts.map +1 -0
- package/dist/types/src/capabilities/app-graph-builder.d.ts +5 -0
- package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
- package/dist/types/src/capabilities/create-object.d.ts +5 -0
- package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +10 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/navigation-resolver.d.ts +5 -0
- package/dist/types/src/capabilities/navigation-resolver.d.ts.map +1 -0
- package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
- package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-surface.d.ts +5 -0
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
- package/dist/types/src/capabilities/routine-templates.d.ts +5 -0
- package/dist/types/src/capabilities/routine-templates.d.ts.map +1 -0
- package/dist/types/src/capabilities/skill-definition.d.ts +5 -0
- package/dist/types/src/capabilities/skill-definition.d.ts.map +1 -0
- package/dist/types/src/components/PostContent/PostContent.d.ts +30 -0
- package/dist/types/src/components/PostContent/PostContent.d.ts.map +1 -0
- package/dist/types/src/components/PostContent/PostContent.test.d.ts +2 -0
- package/dist/types/src/components/PostContent/PostContent.test.d.ts.map +1 -0
- package/dist/types/src/components/PostContent/index.d.ts +2 -0
- package/dist/types/src/components/PostContent/index.d.ts.map +1 -0
- package/dist/types/src/components/PostStack/PostStack.d.ts +17 -0
- package/dist/types/src/components/PostStack/PostStack.d.ts.map +1 -0
- package/dist/types/src/components/PostStack/PostStack.stories.d.ts +10 -0
- package/dist/types/src/components/PostStack/PostStack.stories.d.ts.map +1 -0
- package/dist/types/src/components/PostStack/index.d.ts +2 -0
- package/dist/types/src/components/PostStack/index.d.ts.map +1 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts +23 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts.map +1 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts +8 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts.map +1 -0
- package/dist/types/src/components/SubscriptionStack/index.d.ts +2 -0
- package/dist/types/src/components/SubscriptionStack/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +4 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts +6 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts +12 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts +10 -0
- package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/index.d.ts +2 -0
- package/dist/types/src/containers/FeedArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts +6 -0
- package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts.map +1 -0
- package/dist/types/src/containers/FeedProperties/index.d.ts +2 -0
- package/dist/types/src/containers/FeedProperties/index.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts +6 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts +9 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts +11 -0
- package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/index.d.ts +2 -0
- package/dist/types/src/containers/MagazineArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts +18 -0
- package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/PostArticle.d.ts +6 -0
- package/dist/types/src/containers/PostArticle/PostArticle.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts +8 -0
- package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/PostToolbar.d.ts +19 -0
- package/dist/types/src/containers/PostArticle/PostToolbar.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/index.d.ts +2 -0
- package/dist/types/src/containers/PostArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/PostCard/PostCard.d.ts +11 -0
- package/dist/types/src/containers/PostCard/PostCard.d.ts.map +1 -0
- package/dist/types/src/containers/PostCard/index.d.ts +2 -0
- package/dist/types/src/containers/PostCard/index.d.ts.map +1 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts +5 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts.map +1 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts +8 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/SubscriptionsArticle/index.d.ts +2 -0
- package/dist/types/src/containers/SubscriptionsArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +7 -0
- package/dist/types/src/containers/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +33 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/operations/clear-magazine.d.ts +10 -0
- package/dist/types/src/operations/clear-magazine.d.ts.map +1 -0
- package/dist/types/src/operations/curate-magazine.d.ts +25 -0
- package/dist/types/src/operations/curate-magazine.d.ts.map +1 -0
- package/dist/types/src/operations/curate-magazine.skill.test.d.ts +2 -0
- package/dist/types/src/operations/curate-magazine.skill.test.d.ts.map +1 -0
- package/dist/types/src/operations/curate-magazine.test.d.ts +2 -0
- package/dist/types/src/operations/curate-magazine.test.d.ts.map +1 -0
- package/dist/types/src/operations/extraction/article.d.ts +37 -0
- package/dist/types/src/operations/extraction/article.d.ts.map +1 -0
- package/dist/types/src/operations/extraction/article.test.d.ts +2 -0
- package/dist/types/src/operations/extraction/article.test.d.ts.map +1 -0
- package/dist/types/src/operations/extraction/index.d.ts +2 -0
- package/dist/types/src/operations/extraction/index.d.ts.map +1 -0
- package/dist/types/src/operations/fetch-article-content.d.ts +5 -0
- package/dist/types/src/operations/fetch-article-content.d.ts.map +1 -0
- package/dist/types/src/operations/index.d.ts +3 -0
- package/dist/types/src/operations/index.d.ts.map +1 -0
- package/dist/types/src/operations/load-post-content.d.ts +8 -0
- package/dist/types/src/operations/load-post-content.d.ts.map +1 -0
- package/dist/types/src/operations/sources/article.d.ts +24 -0
- package/dist/types/src/operations/sources/article.d.ts.map +1 -0
- package/dist/types/src/operations/sources/cors.d.ts +12 -0
- package/dist/types/src/operations/sources/cors.d.ts.map +1 -0
- package/dist/types/src/operations/sources/feed-fetcher.d.ts +22 -0
- package/dist/types/src/operations/sources/feed-fetcher.d.ts.map +1 -0
- package/dist/types/src/operations/sources/feed-fetcher.test.d.ts +2 -0
- package/dist/types/src/operations/sources/feed-fetcher.test.d.ts.map +1 -0
- package/dist/types/src/operations/sources/http.d.ts +9 -0
- package/dist/types/src/operations/sources/http.d.ts.map +1 -0
- package/dist/types/src/operations/sources/index.d.ts +6 -0
- package/dist/types/src/operations/sources/index.d.ts.map +1 -0
- package/dist/types/src/operations/sources/rss.d.ts +4 -0
- package/dist/types/src/operations/sources/rss.d.ts.map +1 -0
- package/dist/types/src/operations/sources/rss.test.d.ts +2 -0
- package/dist/types/src/operations/sources/rss.test.d.ts.map +1 -0
- package/dist/types/src/operations/sources/standard-site.d.ts +33 -0
- package/dist/types/src/operations/sources/standard-site.d.ts.map +1 -0
- package/dist/types/src/operations/sync-feed.d.ts +5 -0
- package/dist/types/src/operations/sync-feed.d.ts.map +1 -0
- package/dist/types/src/operations/sync-feed.test.d.ts +2 -0
- package/dist/types/src/operations/sync-feed.test.d.ts.map +1 -0
- package/dist/types/src/operations/util.d.ts +25 -0
- package/dist/types/src/operations/util.d.ts.map +1 -0
- package/dist/types/src/operations/util.test.d.ts +2 -0
- package/dist/types/src/operations/util.test.d.ts.map +1 -0
- package/dist/types/src/paths.d.ts +3 -0
- package/dist/types/src/paths.d.ts.map +1 -0
- package/dist/types/src/plugin.d.ts +4 -0
- package/dist/types/src/plugin.d.ts.map +1 -0
- package/dist/types/src/plugin.workerd.d.ts +3 -0
- package/dist/types/src/plugin.workerd.d.ts.map +1 -0
- package/dist/types/src/skills/index.d.ts +2 -0
- package/dist/types/src/skills/index.d.ts.map +1 -0
- package/dist/types/src/skills/magazine-skill.d.ts +4 -0
- package/dist/types/src/skills/magazine-skill.d.ts.map +1 -0
- package/dist/types/src/stories/ArticleExtractor.stories.d.ts +267 -0
- package/dist/types/src/stories/ArticleExtractor.stories.d.ts.map +1 -0
- package/dist/types/src/stories/MagazineCurate.stories.d.ts +32 -0
- package/dist/types/src/stories/MagazineCurate.stories.d.ts.map +1 -0
- package/dist/types/src/stories/theregister-fixture.test.d.ts +2 -0
- package/dist/types/src/stories/theregister-fixture.test.d.ts.map +1 -0
- package/dist/types/src/templates/magazine-curation.d.ts +7 -0
- package/dist/types/src/templates/magazine-curation.d.ts.map +1 -0
- package/dist/types/src/testing/builder.d.ts +28 -0
- package/dist/types/src/testing/builder.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +247 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types/CreateSubscription.d.ts +29 -0
- package/dist/types/src/types/CreateSubscription.d.ts.map +1 -0
- package/dist/types/src/types/FeedOperation.d.ts +52 -0
- package/dist/types/src/types/FeedOperation.d.ts.map +1 -0
- package/dist/types/src/types/Magazine.d.ts +87 -0
- package/dist/types/src/types/Magazine.d.ts.map +1 -0
- package/dist/types/src/types/Magazine.test.d.ts +2 -0
- package/dist/types/src/types/Magazine.test.d.ts.map +1 -0
- package/dist/types/src/types/Subscription.d.ts +144 -0
- package/dist/types/src/types/Subscription.d.ts.map +1 -0
- package/dist/types/src/types/Subscription.test.d.ts +2 -0
- package/dist/types/src/types/Subscription.test.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +5 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/src/util/date.d.ts +8 -0
- package/dist/types/src/util/date.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +4 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/post-content.d.ts +14 -0
- package/dist/types/src/util/post-content.d.ts.map +1 -0
- package/dist/types/src/util/text.d.ts +18 -0
- package/dist/types/src/util/text.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dx.config.ts +48 -0
- package/package.json +164 -0
- package/src/MagazinePlugin.test.ts +36 -0
- package/src/MagazinePlugin.tsx +61 -0
- package/src/MagazinePlugin.workerd.ts +31 -0
- package/src/atoms/atoms.test.ts +323 -0
- package/src/atoms/index.ts +10 -0
- package/src/atoms/magazine-posts.ts +70 -0
- package/src/atoms/post-content.ts +34 -0
- package/src/atoms/post-curation.ts +36 -0
- package/src/atoms/post-display.ts +59 -0
- package/src/atoms/post-read.ts +40 -0
- package/src/atoms/post-tags.ts +66 -0
- package/src/capabilities/app-graph-builder.ts +125 -0
- package/src/capabilities/create-object.ts +127 -0
- package/src/capabilities/index.ts +17 -0
- package/src/capabilities/navigation-resolver.ts +21 -0
- package/src/capabilities/operation-handler.ts +16 -0
- package/src/capabilities/react-surface.tsx +53 -0
- package/src/capabilities/routine-templates.ts +16 -0
- package/src/capabilities/skill-definition.ts +14 -0
- package/src/components/PostContent/PostContent.test.ts +81 -0
- package/src/components/PostContent/PostContent.tsx +111 -0
- package/src/components/PostContent/index.ts +5 -0
- package/src/components/PostStack/PostStack.stories.tsx +39 -0
- package/src/components/PostStack/PostStack.tsx +151 -0
- package/src/components/PostStack/index.ts +5 -0
- package/src/components/SubscriptionStack/SubscriptionStack.stories.tsx +52 -0
- package/src/components/SubscriptionStack/SubscriptionStack.tsx +161 -0
- package/src/components/SubscriptionStack/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/containers/FeedArticle/FeedArticle.stories.tsx +102 -0
- package/src/containers/FeedArticle/FeedArticle.tsx +68 -0
- package/src/containers/FeedArticle/FeedToolbar.tsx +47 -0
- package/src/containers/FeedArticle/index.ts +5 -0
- package/src/containers/FeedProperties/FeedProperties.tsx +107 -0
- package/src/containers/FeedProperties/index.ts +5 -0
- package/src/containers/MagazineArticle/MagazineArticle.stories.tsx +244 -0
- package/src/containers/MagazineArticle/MagazineArticle.tsx +146 -0
- package/src/containers/MagazineArticle/MagazineTile.tsx +88 -0
- package/src/containers/MagazineArticle/index.ts +5 -0
- package/src/containers/MagazineArticle/useToolbar.tsx +145 -0
- package/src/containers/PostArticle/PostArticle.stories.tsx +117 -0
- package/src/containers/PostArticle/PostArticle.tsx +140 -0
- package/src/containers/PostArticle/PostToolbar.tsx +120 -0
- package/src/containers/PostArticle/index.ts +5 -0
- package/src/containers/PostCard/PostCard.tsx +70 -0
- package/src/containers/PostCard/index.ts +5 -0
- package/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.tsx +80 -0
- package/src/containers/SubscriptionsArticle/SubscriptionsArticle.tsx +99 -0
- package/src/containers/SubscriptionsArticle/index.ts +5 -0
- package/src/containers/index.ts +11 -0
- package/src/index.ts +6 -0
- package/src/meta.ts +9 -0
- package/src/operations/clear-magazine.ts +83 -0
- package/src/operations/curate-magazine.skill.conversations.json +1 -0
- package/src/operations/curate-magazine.skill.test.ts +167 -0
- package/src/operations/curate-magazine.test.ts +91 -0
- package/src/operations/curate-magazine.ts +178 -0
- package/src/operations/extraction/article.test.ts +154 -0
- package/src/operations/extraction/article.ts +270 -0
- package/src/operations/extraction/index.ts +5 -0
- package/src/operations/fetch-article-content.ts +27 -0
- package/src/operations/index.ts +13 -0
- package/src/operations/load-post-content.ts +76 -0
- package/src/operations/sources/article.ts +122 -0
- package/src/operations/sources/cors.ts +19 -0
- package/src/operations/sources/feed-fetcher.test.ts +200 -0
- package/src/operations/sources/feed-fetcher.ts +26 -0
- package/src/operations/sources/http.ts +42 -0
- package/src/operations/sources/index.ts +9 -0
- package/src/operations/sources/rss.test.ts +201 -0
- package/src/operations/sources/rss.ts +138 -0
- package/src/operations/sources/standard-site.ts +381 -0
- package/src/operations/sources/testing/feed.xml +477 -0
- package/src/operations/sync-feed.test.ts +146 -0
- package/src/operations/sync-feed.ts +140 -0
- package/src/operations/util.test.ts +67 -0
- package/src/operations/util.ts +65 -0
- package/src/paths.ts +14 -0
- package/src/plugin.ts +11 -0
- package/src/plugin.workerd.ts +6 -0
- package/src/skills/index.ts +5 -0
- package/src/skills/magazine-skill.ts +45 -0
- package/src/stories/ArticleExtractor.stories.tsx +204 -0
- package/src/stories/MagazineCurate.stories.tsx +217 -0
- package/src/stories/fixtures/theregister-ai.xml +45 -0
- package/src/stories/theregister-fixture.test.ts +35 -0
- package/src/templates/magazine-curation.ts +47 -0
- package/src/testing/builder.ts +77 -0
- package/src/testing/index.ts +5 -0
- package/src/translations.ts +81 -0
- package/src/types/CreateSubscription.ts +61 -0
- package/src/types/FeedOperation.ts +134 -0
- package/src/types/Magazine.test.ts +69 -0
- package/src/types/Magazine.ts +214 -0
- package/src/types/Subscription.test.ts +171 -0
- package/src/types/Subscription.ts +366 -0
- package/src/types/index.ts +8 -0
- package/src/util/date.ts +27 -0
- package/src/util/index.ts +7 -0
- package/src/util/post-content.ts +19 -0
- package/src/util/text.ts +98 -0
- package/src/vite-env.d.ts +10 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, it } from '@effect/vitest';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import { afterEach, beforeEach, vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { Operation } from '@dxos/compute';
|
|
10
|
+
import { Database, Feed, Filter, Ref, Tag } from '@dxos/echo';
|
|
11
|
+
import { TestHelpers } from '@dxos/effect/testing';
|
|
12
|
+
import { AssistantTestLayer } from '@dxos/functions-runtime/testing';
|
|
13
|
+
import { EntityId } from '@dxos/keys';
|
|
14
|
+
|
|
15
|
+
import { FeedOperation, Subscription } from '../types';
|
|
16
|
+
import { MagazineOperationHandlerSet } from './index';
|
|
17
|
+
|
|
18
|
+
EntityId.dangerouslyDisableRandomness();
|
|
19
|
+
|
|
20
|
+
const TestLayer = AssistantTestLayer({
|
|
21
|
+
operationHandlers: MagazineOperationHandlerSet,
|
|
22
|
+
types: [Feed.Feed, Subscription.Subscription, Subscription.Post, Tag.Tag],
|
|
23
|
+
disableLlmMemoization: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Canned RSS XML returned by the stubbed global `fetch`. Two items with stable
|
|
28
|
+
* guids — mirrors what an upstream RSS feed serves on repeated polls when no
|
|
29
|
+
* new posts have been published.
|
|
30
|
+
*/
|
|
31
|
+
const RSS_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
32
|
+
<rss version="2.0">
|
|
33
|
+
<channel>
|
|
34
|
+
<title>Test Feed</title>
|
|
35
|
+
<description>A test feed</description>
|
|
36
|
+
<item>
|
|
37
|
+
<title>Post A</title>
|
|
38
|
+
<link>https://example.com/a</link>
|
|
39
|
+
<guid>post-a</guid>
|
|
40
|
+
<description>A body</description>
|
|
41
|
+
<pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate>
|
|
42
|
+
</item>
|
|
43
|
+
<item>
|
|
44
|
+
<title>Post B</title>
|
|
45
|
+
<link>https://example.com/b</link>
|
|
46
|
+
<guid>post-b</guid>
|
|
47
|
+
<description>B body</description>
|
|
48
|
+
<pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate>
|
|
49
|
+
</item>
|
|
50
|
+
</channel>
|
|
51
|
+
</rss>`;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* End-to-end tests for the {@link FeedOperation.SyncFeed} operation against
|
|
55
|
+
* the full operation harness (Database + Feed services + handler registry).
|
|
56
|
+
*
|
|
57
|
+
* Stubs only the network boundary (`globalThis.fetch`) so the test exercises
|
|
58
|
+
* the real RSS parser, real ECHO writes, and the real operation runtime.
|
|
59
|
+
*
|
|
60
|
+
* The original bug: each sync re-appended every previously-seen post because
|
|
61
|
+
* the cursor filter `post.guid !== cursor` only excluded the single post whose
|
|
62
|
+
* guid matched the cursor — every older post passed through and was appended
|
|
63
|
+
* again. These tests pin the corrected, guid-deduped behavior.
|
|
64
|
+
*/
|
|
65
|
+
describe('SyncFeed', () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
vi.stubGlobal(
|
|
68
|
+
'fetch',
|
|
69
|
+
vi.fn(async () => new Response(RSS_XML, { headers: { 'content-type': 'application/xml' } })),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
vi.unstubAllGlobals();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it.effect(
|
|
78
|
+
're-syncing the same upstream response does not duplicate posts',
|
|
79
|
+
Effect.fnUntraced(
|
|
80
|
+
function* ({ expect }) {
|
|
81
|
+
const subscriptionFeed = yield* Database.add(
|
|
82
|
+
Subscription.makeSubscription({ name: 'feed', url: 'https://example.com/rss' }),
|
|
83
|
+
);
|
|
84
|
+
yield* Database.flush();
|
|
85
|
+
const echoFeed = yield* Database.load(subscriptionFeed.feed);
|
|
86
|
+
|
|
87
|
+
yield* Operation.invoke(FeedOperation.SyncFeed, { feed: Ref.make(subscriptionFeed) });
|
|
88
|
+
yield* Operation.invoke(FeedOperation.SyncFeed, { feed: Ref.make(subscriptionFeed) });
|
|
89
|
+
|
|
90
|
+
const items = yield* Feed.query(echoFeed, Filter.type(Subscription.Post)).run;
|
|
91
|
+
expect(items).toHaveLength(2);
|
|
92
|
+
const guids = items.map((post) => post.guid).sort();
|
|
93
|
+
expect(guids).toEqual(['post-a', 'post-b']);
|
|
94
|
+
},
|
|
95
|
+
Effect.provide(TestLayer),
|
|
96
|
+
TestHelpers.provideTestContext,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
it.effect(
|
|
101
|
+
'within-batch and link-fallback dedup: same link served twice in one response is appended once',
|
|
102
|
+
Effect.fnUntraced(
|
|
103
|
+
function* ({ expect }) {
|
|
104
|
+
// RSS items with NO `<guid>` element — the dedup key falls back to
|
|
105
|
+
// `link`. Two items share the same link, simulating a misbehaving
|
|
106
|
+
// upstream that serves the same post twice in one response.
|
|
107
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
108
|
+
<rss version="2.0">
|
|
109
|
+
<channel>
|
|
110
|
+
<title>Duplicate Feed</title>
|
|
111
|
+
<item>
|
|
112
|
+
<title>First copy</title>
|
|
113
|
+
<link>https://example.com/shared</link>
|
|
114
|
+
<description>body</description>
|
|
115
|
+
</item>
|
|
116
|
+
<item>
|
|
117
|
+
<title>Second copy</title>
|
|
118
|
+
<link>https://example.com/shared</link>
|
|
119
|
+
<description>body</description>
|
|
120
|
+
</item>
|
|
121
|
+
</channel>
|
|
122
|
+
</rss>`;
|
|
123
|
+
vi.stubGlobal(
|
|
124
|
+
'fetch',
|
|
125
|
+
vi.fn(async () => new Response(xml, { headers: { 'content-type': 'application/xml' } })),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const subscriptionFeed = yield* Database.add(
|
|
129
|
+
Subscription.makeSubscription({ name: 'feed', url: 'https://example.com/rss' }),
|
|
130
|
+
);
|
|
131
|
+
yield* Database.flush();
|
|
132
|
+
const echoFeed = yield* Database.load(subscriptionFeed.feed);
|
|
133
|
+
|
|
134
|
+
yield* Operation.invoke(FeedOperation.SyncFeed, { feed: Ref.make(subscriptionFeed) });
|
|
135
|
+
// Second sync also exercises the persisted-post dedup path for link-keyed items.
|
|
136
|
+
yield* Operation.invoke(FeedOperation.SyncFeed, { feed: Ref.make(subscriptionFeed) });
|
|
137
|
+
|
|
138
|
+
const items = yield* Feed.query(echoFeed, Filter.type(Subscription.Post)).run;
|
|
139
|
+
expect(items).toHaveLength(1);
|
|
140
|
+
expect(items[0].link).toBe('https://example.com/shared');
|
|
141
|
+
},
|
|
142
|
+
Effect.provide(TestLayer),
|
|
143
|
+
TestHelpers.provideTestContext,
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import { Operation } from '@dxos/compute';
|
|
8
|
+
import { Database, Feed, Filter, Obj, Ref } from '@dxos/echo';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
|
|
11
|
+
import { FeedOperation, Subscription } from '../types';
|
|
12
|
+
import { type FeedFetcher, browserCorsProxy, fetchRss, fetchStandardSite } from './sources';
|
|
13
|
+
|
|
14
|
+
/** Stable dedup key for a {@link Subscription.Post}. Both fields are optional, but every current fetcher populates `guid` (RSS falls back to `link`, Standard.site uses the record AT-URI). */
|
|
15
|
+
const postKey = (post: { guid?: string; link?: string }): string | undefined => post.guid ?? post.link;
|
|
16
|
+
|
|
17
|
+
/** Resolves the appropriate fetcher for the given feed type. */
|
|
18
|
+
const getFetcher = (type: Subscription.FeedType | undefined): FeedFetcher => {
|
|
19
|
+
switch (type) {
|
|
20
|
+
case 'standard-site':
|
|
21
|
+
return fetchStandardSite;
|
|
22
|
+
case 'rss':
|
|
23
|
+
default:
|
|
24
|
+
return fetchRss;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handler: Operation.WithHandler<typeof FeedOperation.SyncFeed> = FeedOperation.SyncFeed.pipe(
|
|
29
|
+
Operation.withHandler(
|
|
30
|
+
Effect.fnUntraced(function* ({ feed }) {
|
|
31
|
+
const subscriptionFeed = yield* Database.load(feed);
|
|
32
|
+
const url = subscriptionFeed.url;
|
|
33
|
+
invariant(url, 'Feed URL is required.');
|
|
34
|
+
const echoFeed = yield* Database.load(subscriptionFeed.feed);
|
|
35
|
+
invariant(echoFeed, 'Backing ECHO feed not found.');
|
|
36
|
+
invariant(Feed.getQueueUri(echoFeed), 'Feed not stored in a space.');
|
|
37
|
+
|
|
38
|
+
const fetcher = getFetcher(subscriptionFeed.type);
|
|
39
|
+
const { feed: feedMeta, posts } = yield* fetcher(url, { corsProxy: browserCorsProxy() });
|
|
40
|
+
|
|
41
|
+
// Dedup against existing posts already in the backing queue. The
|
|
42
|
+
// `cursor` field on the subscription was previously used as a single-guid
|
|
43
|
+
// filter, but RSS feeds re-serve the same items across polls — filtering
|
|
44
|
+
// out only the cursor's own guid let every other previously-synced post
|
|
45
|
+
// re-pass the filter and re-append on each sync. Key-based dedup against
|
|
46
|
+
// the queue is the source of truth and tolerant of out-of-order or
|
|
47
|
+
// rotated-off-the-window upstream responses.
|
|
48
|
+
//
|
|
49
|
+
// The dedup key is `guid ?? link`: `Post.guid` is optional in the schema,
|
|
50
|
+
// and while every current fetcher populates it (RSS falls back to `link`,
|
|
51
|
+
// Standard.site uses the record AT-URI), a future fetcher or malformed item
|
|
52
|
+
// could omit it. Falling back to `link` keeps such items dedup-able. Items
|
|
53
|
+
// with neither field cannot be deduplicated and will sync as new every
|
|
54
|
+
// time — that's an upstream data quality issue with no clean recovery.
|
|
55
|
+
//
|
|
56
|
+
// Pull the backing queue before reading existing posts: this handler runs in a
|
|
57
|
+
// freshly-spawned process whose local queue replica may not have the blocks yet. Without
|
|
58
|
+
// this, the query returns an empty set, `seenKeys` is empty, and every fetched item
|
|
59
|
+
// re-appends — duplicating the whole feed on a cold sync.
|
|
60
|
+
yield* Feed.sync(echoFeed, { shouldPush: false });
|
|
61
|
+
const existing = yield* Feed.query(echoFeed, Filter.type(Subscription.Post)).run;
|
|
62
|
+
const seenKeys = new Set<string>();
|
|
63
|
+
for (const post of existing) {
|
|
64
|
+
const key = postKey(post);
|
|
65
|
+
if (key) {
|
|
66
|
+
seenKeys.add(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const newPosts: typeof posts = [];
|
|
70
|
+
for (const post of posts) {
|
|
71
|
+
const key = postKey(post);
|
|
72
|
+
// Within-batch dedup: if the upstream serves the same item twice in one
|
|
73
|
+
// response, only the first occurrence is appended.
|
|
74
|
+
if (key && seenKeys.has(key)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (key) {
|
|
78
|
+
seenKeys.add(key);
|
|
79
|
+
}
|
|
80
|
+
newPosts.push(post);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Append new posts to the ECHO feed.
|
|
84
|
+
// NOTE: The `Subscription.Subscription.keep` bound is currently NOT enforced
|
|
85
|
+
// here via `Feed.remove()`. Doing so wipes the underlying queue's
|
|
86
|
+
// `_objectCache` for the deleted posts, but those same Post objects
|
|
87
|
+
// persist in `space.db` (they were added there when first curated
|
|
88
|
+
// into a Magazine via `createRef` → `database.add`). On the next
|
|
89
|
+
// sync/curate, `Feed.query(...).run` returns fresh proxies for the
|
|
90
|
+
// kept items, and any magazine refs to *deleted* posts now
|
|
91
|
+
// reference proxies whose `_internals.database` link is unset —
|
|
92
|
+
// `createRef` then tries to re-add them, hitting the
|
|
93
|
+
// `!_objects.has(core.id)` invariant in `EntityManager.addCore`.
|
|
94
|
+
// Until feed/db lifecycle is reworked we leave the feed unbounded;
|
|
95
|
+
// the `Magazine.keep` bound (enforced in
|
|
96
|
+
// `MagazineArticle.handleCurate`) prevents the visible list from
|
|
97
|
+
// growing unboundedly.
|
|
98
|
+
if (newPosts.length > 0) {
|
|
99
|
+
const feedRef = Ref.make(subscriptionFeed);
|
|
100
|
+
const postObjects = newPosts.map((post) =>
|
|
101
|
+
Obj.make(Subscription.Post, {
|
|
102
|
+
source: feedRef,
|
|
103
|
+
title: post.title,
|
|
104
|
+
link: post.link,
|
|
105
|
+
description: post.description,
|
|
106
|
+
// Persist the body so Standard.site articles render offline (see `load-post-content`);
|
|
107
|
+
// RSS `content:encoded` rides along but its fetch path is unchanged.
|
|
108
|
+
content: post.content,
|
|
109
|
+
author: post.author,
|
|
110
|
+
published: post.published,
|
|
111
|
+
guid: post.guid,
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
yield* Feed.append(echoFeed, postObjects);
|
|
115
|
+
|
|
116
|
+
// Advance cursor to the newest post.
|
|
117
|
+
const newestGuid = posts[0]?.guid;
|
|
118
|
+
if (newestGuid) {
|
|
119
|
+
Obj.update(subscriptionFeed, (subscriptionFeed) => {
|
|
120
|
+
subscriptionFeed.cursor = newestGuid;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Update feed metadata from channel if not already set.
|
|
125
|
+
if (feedMeta.name && !subscriptionFeed.name) {
|
|
126
|
+
Obj.update(subscriptionFeed, (subscriptionFeed) => {
|
|
127
|
+
subscriptionFeed.name = feedMeta.name;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (feedMeta.description && !subscriptionFeed.description) {
|
|
131
|
+
Obj.update(subscriptionFeed, (subscriptionFeed) => {
|
|
132
|
+
subscriptionFeed.description = feedMeta.description;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
export default handler;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { Obj } from '@dxos/echo';
|
|
8
|
+
|
|
9
|
+
import { Subscription } from '../types';
|
|
10
|
+
import { partitionByKeepBound } from './util';
|
|
11
|
+
|
|
12
|
+
const makePost = (props: { title: string; published?: string }): Subscription.Post =>
|
|
13
|
+
Obj.make(Subscription.Post, { title: props.title, published: props.published });
|
|
14
|
+
|
|
15
|
+
describe('partitionByKeepBound', () => {
|
|
16
|
+
test('keeps the newest N non-starred posts and drops the rest', () => {
|
|
17
|
+
const posts = [
|
|
18
|
+
makePost({ title: 'A', published: '2026-04-01T00:00:00Z' }),
|
|
19
|
+
makePost({ title: 'B', published: '2026-04-02T00:00:00Z' }),
|
|
20
|
+
makePost({ title: 'C', published: '2026-04-03T00:00:00Z' }),
|
|
21
|
+
makePost({ title: 'D', published: '2026-04-04T00:00:00Z' }),
|
|
22
|
+
makePost({ title: 'E', published: '2026-04-05T00:00:00Z' }),
|
|
23
|
+
];
|
|
24
|
+
const { kept, dropped } = partitionByKeepBound(posts, 2, () => false);
|
|
25
|
+
// Keep newest two (E, D); drop the older three.
|
|
26
|
+
expect(kept.map((post) => post.title).sort()).toEqual(['D', 'E']);
|
|
27
|
+
expect(dropped.map((post) => post.title).sort()).toEqual(['A', 'B', 'C']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('preserves starred posts beyond the keep bound', () => {
|
|
31
|
+
const posts = [
|
|
32
|
+
makePost({ title: 'A-old-starred', published: '2026-04-01T00:00:00Z' }),
|
|
33
|
+
makePost({ title: 'B', published: '2026-04-02T00:00:00Z' }),
|
|
34
|
+
makePost({ title: 'C', published: '2026-04-03T00:00:00Z' }),
|
|
35
|
+
makePost({ title: 'D', published: '2026-04-04T00:00:00Z' }),
|
|
36
|
+
makePost({ title: 'E', published: '2026-04-05T00:00:00Z' }),
|
|
37
|
+
];
|
|
38
|
+
const isStarred = (post: Subscription.Post) => post.title === 'A-old-starred';
|
|
39
|
+
const { kept, dropped } = partitionByKeepBound(posts, 2, isStarred);
|
|
40
|
+
// Starred A always kept; newest non-starred E and D fill the bound; B and C dropped.
|
|
41
|
+
expect(kept.map((post) => post.title).sort()).toEqual(['A-old-starred', 'D', 'E']);
|
|
42
|
+
expect(dropped.map((post) => post.title).sort()).toEqual(['B', 'C']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('falls back gracefully when posts have no published date', () => {
|
|
46
|
+
const posts = [
|
|
47
|
+
makePost({ title: 'A' }),
|
|
48
|
+
makePost({ title: 'B', published: '2026-04-02T00:00:00Z' }),
|
|
49
|
+
makePost({ title: 'C', published: '2026-04-03T00:00:00Z' }),
|
|
50
|
+
];
|
|
51
|
+
const { kept, dropped } = partitionByKeepBound(posts, 1, () => false);
|
|
52
|
+
// Only the newest dated post is kept; the undated post sorts to the bottom.
|
|
53
|
+
expect(kept.map((post) => post.title)).toEqual(['C']);
|
|
54
|
+
expect(dropped.map((post) => post.title).sort()).toEqual(['A', 'B']);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('keep bound of 0 drops all non-starred posts', () => {
|
|
58
|
+
const posts = [
|
|
59
|
+
makePost({ title: 'A', published: '2026-04-01T00:00:00Z' }),
|
|
60
|
+
makePost({ title: 'B-starred', published: '2026-04-02T00:00:00Z' }),
|
|
61
|
+
];
|
|
62
|
+
const isStarred = (post: Subscription.Post) => post.title === 'B-starred';
|
|
63
|
+
const { kept, dropped } = partitionByKeepBound(posts, 0, isStarred);
|
|
64
|
+
expect(kept.map((post) => post.title)).toEqual(['B-starred']);
|
|
65
|
+
expect(dropped.map((post) => post.title)).toEqual(['A']);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import { Database, Filter, Query } from '@dxos/echo';
|
|
8
|
+
|
|
9
|
+
import { type Magazine, Subscription } from '../types';
|
|
10
|
+
import { publishedTimestamp } from '../util/date';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolves the Magazine's referenced feeds and the Posts in each feed's backing
|
|
14
|
+
* ECHO queue, filtering out Posts already curated into `magazine.posts` (and cross-feed duplicates).
|
|
15
|
+
*/
|
|
16
|
+
export const collectCandidates = (magazine: Magazine.Magazine) =>
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const seen = new Set(magazine.posts.map((ref) => ref.target?.id ?? ref.uri));
|
|
19
|
+
const candidates: Array<{ post: Subscription.Post; feed: Subscription.Subscription }> = [];
|
|
20
|
+
for (const feedRef of magazine.feeds) {
|
|
21
|
+
const feed = yield* Database.load(feedRef);
|
|
22
|
+
const echoFeed = feed.feed?.target;
|
|
23
|
+
if (!echoFeed) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const posts = yield* Database.query(Query.select(Filter.type(Subscription.Post)).from(echoFeed)).run;
|
|
27
|
+
for (const post of posts) {
|
|
28
|
+
if (seen.has(post.id)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
seen.add(post.id);
|
|
32
|
+
candidates.push({ post, feed });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return candidates;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Partitions posts into those to keep and those to drop, given a maximum
|
|
40
|
+
* non-starred retention bound. Starred posts are always kept; the remaining
|
|
41
|
+
* non-starred posts are sorted newest-first by `published` and the top
|
|
42
|
+
* `keep` retained.
|
|
43
|
+
*
|
|
44
|
+
* `isStarred` is injectable; callers with a starred-tag uri pass a predicate backed by it
|
|
45
|
+
* (defaults to treating nothing as starred).
|
|
46
|
+
*/
|
|
47
|
+
export const partitionByKeepBound = <T extends Subscription.Post>(
|
|
48
|
+
posts: readonly T[],
|
|
49
|
+
keep: number,
|
|
50
|
+
isStarred: (post: T) => boolean = () => false,
|
|
51
|
+
): { kept: T[]; dropped: T[] } => {
|
|
52
|
+
const kept: T[] = [];
|
|
53
|
+
const candidates: T[] = [];
|
|
54
|
+
for (const post of posts) {
|
|
55
|
+
if (isStarred(post)) {
|
|
56
|
+
kept.push(post);
|
|
57
|
+
} else {
|
|
58
|
+
candidates.push(post);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
candidates.sort((a, b) => publishedTimestamp(b.published) - publishedTimestamp(a.published));
|
|
62
|
+
const retained = candidates.slice(0, Math.max(0, keep));
|
|
63
|
+
const dropped = candidates.slice(Math.max(0, keep));
|
|
64
|
+
return { kept: [...kept, ...retained], dropped };
|
|
65
|
+
};
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Paths } from '@dxos/app-toolkit';
|
|
6
|
+
|
|
7
|
+
import { Magazine } from '#types';
|
|
8
|
+
|
|
9
|
+
const { getSectionPath: getMagazinesPath, getObjectPath: getMagazinePath } = Paths.createTypeSectionPaths(
|
|
10
|
+
Magazine.Magazine,
|
|
11
|
+
{ groupId: Paths.GroupSegments.content },
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export { getMagazinePath, getMagazinesPath };
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Plugin } from '@dxos/app-framework';
|
|
6
|
+
|
|
7
|
+
import { meta } from './meta';
|
|
8
|
+
|
|
9
|
+
export const MagazinePlugin = Plugin.lazy(meta, () => import('#plugin'));
|
|
10
|
+
|
|
11
|
+
export { MagazineOperationHandlerSet } from './operations';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Skill, Template } from '@dxos/compute';
|
|
6
|
+
import { trim } from '@dxos/util';
|
|
7
|
+
|
|
8
|
+
import { FeedOperation } from '#types';
|
|
9
|
+
import { Magazine } from '#types';
|
|
10
|
+
|
|
11
|
+
const operations = [FeedOperation.FetchArticleContent];
|
|
12
|
+
|
|
13
|
+
const make = () =>
|
|
14
|
+
Skill.make({
|
|
15
|
+
key: Magazine.SKILL_KEY,
|
|
16
|
+
name: 'Magazine Curator',
|
|
17
|
+
tools: Skill.toolDefinitions({ operations }),
|
|
18
|
+
instructions: Template.make({
|
|
19
|
+
// HOW only: input shape, tool usage, and the completeJob output contract. The editorial
|
|
20
|
+
// methodology (what to select, dedup, snippet/image guidance) and the magazine's topic live on
|
|
21
|
+
// the Routine's instructions (see Magazine.DEFAULT_INSTRUCTIONS), not here.
|
|
22
|
+
source: trim`
|
|
23
|
+
The candidate Posts are provided in the <input> as a JSON array under "candidates"; each
|
|
24
|
+
candidate has an "id" plus title, description, author, feedName, published, and link. The
|
|
25
|
+
editorial brief (the Topic and selection guidance) is provided in your instructions.
|
|
26
|
+
|
|
27
|
+
When a candidate's title and description are not enough to judge relevance, you MAY call
|
|
28
|
+
fetchArticleContent with the candidate's id to read the full article text (and find an image)
|
|
29
|
+
before deciding.
|
|
30
|
+
|
|
31
|
+
When you are done, call completeJob with the structured output:
|
|
32
|
+
{ "posts": [{ "id": "<candidate id>", "snippet": "...", "imageUrl": "..." }, ...] }
|
|
33
|
+
listing the selected candidates in the order you want them shown. Only use ids that appear
|
|
34
|
+
in the input — never invent ids. Omit "imageUrl" when no image is available. Do not attempt to
|
|
35
|
+
add Posts yourself; the Magazine is updated mechanically from your output.
|
|
36
|
+
`,
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const skill: Skill.Definition = {
|
|
41
|
+
key: Magazine.SKILL_KEY,
|
|
42
|
+
make,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default skill;
|