@recursiv/sdk 0.7.39 → 0.7.40
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/README.md +14 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/resources/discovery-learning.d.ts +27 -0
- package/dist/resources/discovery-learning.js +58 -0
- package/dist/resources/discovery.d.ts +69 -0
- package/dist/resources/discovery.js +156 -0
- package/dist/resources/protocols.d.ts +6 -1
- package/dist/resources/protocols.js +4 -2
- package/dist/types.d.ts +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -595,3 +595,17 @@ Standalone public text publication uses `protocols.prepareNativePost({ clientMut
|
|
|
595
595
|
For Bluesky accounts advertising `quote`, prepare the same publication with `quote: { protocol: 'bluesky', nativeId: sourceAtUri }`. The server resolves the exact source CID using each connected account, checks quote permission and Minds safety, and rechecks before writing. Review each delivery’s `quote` alongside the account and comment before submitting. Changed or unavailable sources cannot publish; uncertain deliveries reconcile the original record key. ActivityPub/Nostr quotes are not provided by this method. Public post reads include at most one filtered `quote` with `state: 'available'` and `post`, or `state: 'unavailable'` without source content. Available quotes may include `embeddedReferences`: exact standalone source-reference lines in the unchanged outer `text`. Omit them only when the quoted original is actually visible; retain them for unavailable or locally hidden quotes. Nostr references are decoded and matched by the server, without following relay hints.
|
|
596
596
|
|
|
597
597
|
Topic browsing can request `getPublicTrends({ protocol: "bluesky", includePreviews: true })`. Optional `SocialTrend.preview` is a real image-bearing post from that topic, never a generated illustration. The server samples at most eight posts for each of ten topics, with three concurrent reads and a shared 2.5-second deadline; missing images leave usable text topics. App moderation and user source controls filter previews on every request. Bluesky CDN image attachments, quote-with-media images, and unlabeled link-preview thumbnails are supported; omitted content retains the source link.
|
|
598
|
+
|
|
599
|
+
### Continuous public discovery
|
|
600
|
+
|
|
601
|
+
Create one `sdk.discovery.createFeed({ networks, category, source, learning })`
|
|
602
|
+
per signed-in viewer and filter. Pass the app-advertised network catalog, then call
|
|
603
|
+
`next()` as the reader approaches the end. Each page contains safe json-render
|
|
604
|
+
module specs and native or protocol post objects. Preserve their existing source
|
|
605
|
+
authority and interactive controls. `retryFailed()` explicitly retries unavailable
|
|
606
|
+
sources; create a new session to refresh exhausted source windows.
|
|
607
|
+
|
|
608
|
+
Optional `DiscoveryLearning` consumes real visible impressions and explicit
|
|
609
|
+
engagement/hide outcomes. Persist its count-only snapshot per viewer; never share
|
|
610
|
+
a session or learning state across accounts. It starts conservatively and does
|
|
611
|
+
not imply measured retention improvement.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export * from './resources/discovery-learning.js';
|
|
2
|
+
import { DiscoveryResource } from './resources/discovery.js';
|
|
3
|
+
export * from './resources/discovery.js';
|
|
1
4
|
import { AuthResource } from './resources/auth.js';
|
|
2
5
|
import { PostsResource } from './resources/posts.js';
|
|
3
6
|
import { UsersResource } from './resources/users.js';
|
|
@@ -113,6 +116,7 @@ export declare class Recursiv {
|
|
|
113
116
|
readonly webhooks: WebhooksResource;
|
|
114
117
|
readonly jobs: JobsResource;
|
|
115
118
|
readonly curator: CuratorResource;
|
|
119
|
+
readonly discovery: DiscoveryResource;
|
|
116
120
|
readonly video: VideoResource;
|
|
117
121
|
readonly appSubscriptions: AppSubscriptionsResource;
|
|
118
122
|
readonly reports: ReportsResource;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export * from './resources/discovery-learning.js';
|
|
2
|
+
import { DiscoveryResource } from './resources/discovery.js';
|
|
3
|
+
export * from './resources/discovery.js';
|
|
1
4
|
import { HttpClient } from './client.js';
|
|
2
5
|
import { AuthResource } from './resources/auth.js';
|
|
3
6
|
import { PostsResource } from './resources/posts.js';
|
|
@@ -114,6 +117,7 @@ export class Recursiv {
|
|
|
114
117
|
webhooks;
|
|
115
118
|
jobs;
|
|
116
119
|
curator;
|
|
120
|
+
discovery;
|
|
117
121
|
video;
|
|
118
122
|
appSubscriptions;
|
|
119
123
|
reports;
|
|
@@ -175,6 +179,7 @@ export class Recursiv {
|
|
|
175
179
|
this.webhooks = new WebhooksResource(client);
|
|
176
180
|
this.jobs = new JobsResource(client);
|
|
177
181
|
this.curator = new CuratorResource(client);
|
|
182
|
+
this.discovery = new DiscoveryResource(this.posts, this.protocols);
|
|
178
183
|
this.video = new VideoResource(client);
|
|
179
184
|
this.appSubscriptions = new AppSubscriptionsResource(client);
|
|
180
185
|
this.reports = new ReportsResource(client);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DiscoveryLayout } from '../types.js';
|
|
2
|
+
export type DiscoveryPresentation = DiscoveryLayout['presentation'];
|
|
3
|
+
type Counts = {
|
|
4
|
+
views: number;
|
|
5
|
+
engagements: number;
|
|
6
|
+
hides: number;
|
|
7
|
+
};
|
|
8
|
+
export interface DiscoveryLearningSnapshot {
|
|
9
|
+
version: 1;
|
|
10
|
+
startedAt: number;
|
|
11
|
+
layouts: Record<DiscoveryPresentation, Counts>;
|
|
12
|
+
}
|
|
13
|
+
/** Local, per-viewer experimental presentation preferences. Stores counts only,
|
|
14
|
+
* never source text, author IDs or inferred traits. It cannot promote content,
|
|
15
|
+
* bypass safety, add unsupported layouts or establish a causal lift claim. */
|
|
16
|
+
export declare class DiscoveryLearning {
|
|
17
|
+
private now;
|
|
18
|
+
private value;
|
|
19
|
+
private observed;
|
|
20
|
+
constructor(input?: unknown, now?: () => number);
|
|
21
|
+
observe(id: string, presentation: DiscoveryPresentation, event: 'view' | 'engage' | 'hide'): void;
|
|
22
|
+
/** Jev proposes a suitable layout. Sufficient recent feedback may restore full
|
|
23
|
+
* post cards; every tenth eligible batch explores the safe proposal again. */
|
|
24
|
+
choose(proposal: DiscoveryPresentation, page: number): DiscoveryPresentation;
|
|
25
|
+
snapshot(): DiscoveryLearningSnapshot;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const names = ['posts', 'visual', 'conversation', 'video'];
|
|
2
|
+
const fresh = (now) => ({ version: 1, startedAt: now, layouts: {
|
|
3
|
+
video: { views: 0, engagements: 0, hides: 0 }, posts: { views: 0, engagements: 0, hides: 0 }, visual: { views: 0, engagements: 0, hides: 0 }, conversation: { views: 0, engagements: 0, hides: 0 },
|
|
4
|
+
} });
|
|
5
|
+
/** Local, per-viewer experimental presentation preferences. Stores counts only,
|
|
6
|
+
* never source text, author IDs or inferred traits. It cannot promote content,
|
|
7
|
+
* bypass safety, add unsupported layouts or establish a causal lift claim. */
|
|
8
|
+
export class DiscoveryLearning {
|
|
9
|
+
now;
|
|
10
|
+
value;
|
|
11
|
+
observed = new Set();
|
|
12
|
+
constructor(input, now = Date.now) {
|
|
13
|
+
this.now = now;
|
|
14
|
+
const value = input;
|
|
15
|
+
const time = now();
|
|
16
|
+
this.value = value?.version === 1 && Number.isFinite(value.startedAt) && value.startedAt <= time
|
|
17
|
+
&& time - value.startedAt < 14 * 86400_000 && names.every(name => {
|
|
18
|
+
const c = value.layouts?.[name];
|
|
19
|
+
return c && [c.views, c.engagements, c.hides].every(n => Number.isSafeInteger(n) && n >= 0 && n <= 10000)
|
|
20
|
+
&& c.engagements <= c.views && c.hides <= c.views;
|
|
21
|
+
}) ? JSON.parse(JSON.stringify(value)) : fresh(time);
|
|
22
|
+
}
|
|
23
|
+
observe(id, presentation, event) {
|
|
24
|
+
if (!names.includes(presentation) || !id || id.length > 8192)
|
|
25
|
+
return;
|
|
26
|
+
const key = `${id}:${event}`;
|
|
27
|
+
if (this.observed.has(key))
|
|
28
|
+
return;
|
|
29
|
+
// Engagements/hides require a real visible impression in this session.
|
|
30
|
+
if (event !== 'view' && !this.observed.has(`${id}:view`))
|
|
31
|
+
return;
|
|
32
|
+
this.observed.add(key);
|
|
33
|
+
while (this.observed.size > 4096)
|
|
34
|
+
this.observed.delete(this.observed.values().next().value);
|
|
35
|
+
const counters = this.value.layouts[presentation];
|
|
36
|
+
counters[event === 'view' ? 'views' : event === 'engage' ? 'engagements' : 'hides']++;
|
|
37
|
+
if (counters.views >= 10000)
|
|
38
|
+
for (const counts of Object.values(this.value.layouts)) {
|
|
39
|
+
counts.views = Math.floor(counts.views / 2);
|
|
40
|
+
counts.engagements = Math.floor(counts.engagements / 2);
|
|
41
|
+
counts.hides = Math.floor(counts.hides / 2);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Jev proposes a suitable layout. Sufficient recent feedback may restore full
|
|
45
|
+
* post cards; every tenth eligible batch explores the safe proposal again. */
|
|
46
|
+
choose(proposal, page) {
|
|
47
|
+
if (proposal === 'posts' || page % 10 === 0)
|
|
48
|
+
return proposal;
|
|
49
|
+
const selected = this.value.layouts[proposal];
|
|
50
|
+
const baseline = this.value.layouts.posts;
|
|
51
|
+
if (selected.views < 50 || baseline.views < 50)
|
|
52
|
+
return proposal;
|
|
53
|
+
const value = (c) => (c.engagements + 2 - 3 * c.hides) / (c.views + 10);
|
|
54
|
+
// Require a material difference, rather than switching on a single click.
|
|
55
|
+
return value(selected) + 0.1 < value(baseline) ? 'posts' : proposal;
|
|
56
|
+
}
|
|
57
|
+
snapshot() { return JSON.parse(JSON.stringify(this.value)); }
|
|
58
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { DiscoveryLearning } from './discovery-learning.js';
|
|
2
|
+
import type { DiscoveryLayout, Post } from '../types.js';
|
|
3
|
+
import type { PostsResource } from './posts.js';
|
|
4
|
+
import type { ProtocolsResource, SocialNetwork, SocialPost } from './protocols.js';
|
|
5
|
+
export type DiscoveryItem = {
|
|
6
|
+
key: string;
|
|
7
|
+
kind: 'minds';
|
|
8
|
+
post: Post;
|
|
9
|
+
} | {
|
|
10
|
+
key: string;
|
|
11
|
+
kind: 'social';
|
|
12
|
+
post: SocialPost;
|
|
13
|
+
};
|
|
14
|
+
/** json-render Spec: values are references to authenticated SDK results, never
|
|
15
|
+
* arbitrary executable components, links, generated text or provider markup. */
|
|
16
|
+
export interface DiscoveryModule {
|
|
17
|
+
id: string;
|
|
18
|
+
presentation: DiscoveryLayout['presentation'];
|
|
19
|
+
source: DiscoveryLayout['source'];
|
|
20
|
+
items: DiscoveryItem[];
|
|
21
|
+
spec: {
|
|
22
|
+
root: 'module';
|
|
23
|
+
elements: {
|
|
24
|
+
module: {
|
|
25
|
+
type: 'DiscoveryPosts' | 'DiscoveryGallery' | 'DiscoveryConversation' | 'DiscoveryVideo';
|
|
26
|
+
props: {
|
|
27
|
+
itemKeys: string[];
|
|
28
|
+
};
|
|
29
|
+
children: [];
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface DiscoveryFeedPage {
|
|
35
|
+
modules: DiscoveryModule[];
|
|
36
|
+
hasMore: boolean;
|
|
37
|
+
failedSources: string[];
|
|
38
|
+
partialCategories: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Per-viewer feed session. Create a new session when user, app, category or
|
|
41
|
+
* source changes. All reads retain existing server authorization/moderation.
|
|
42
|
+
* At most four requests per page; providers fail independently. */
|
|
43
|
+
export declare class DiscoveryFeedSession {
|
|
44
|
+
private posts;
|
|
45
|
+
private protocols;
|
|
46
|
+
private category?;
|
|
47
|
+
private learning?;
|
|
48
|
+
private streams;
|
|
49
|
+
private pending;
|
|
50
|
+
private seen;
|
|
51
|
+
private page;
|
|
52
|
+
private nextSource;
|
|
53
|
+
constructor(posts: Pick<PostsResource, 'list'>, protocols: Pick<ProtocolsResource, 'getPublicTrends' | 'getPublicTrendFeed'>, networks: SocialNetwork[], category?: string | undefined, source?: string, learning?: DiscoveryLearning | undefined);
|
|
54
|
+
retryFailed(): void;
|
|
55
|
+
next(): Promise<DiscoveryFeedPage>;
|
|
56
|
+
private read;
|
|
57
|
+
private readPage;
|
|
58
|
+
}
|
|
59
|
+
export declare class DiscoveryResource {
|
|
60
|
+
private posts;
|
|
61
|
+
private protocols;
|
|
62
|
+
constructor(posts: PostsResource, protocols: ProtocolsResource);
|
|
63
|
+
createFeed(options: {
|
|
64
|
+
networks: SocialNetwork[];
|
|
65
|
+
category?: string;
|
|
66
|
+
source?: string;
|
|
67
|
+
learning?: DiscoveryLearning;
|
|
68
|
+
}): DiscoveryFeedSession;
|
|
69
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const ordinary = { version: 'discovery-layout-v1', presentation: 'posts', source: 'fallback' };
|
|
2
|
+
const key = (protocol, id) => JSON.stringify([protocol, id]);
|
|
3
|
+
/** Per-viewer feed session. Create a new session when user, app, category or
|
|
4
|
+
* source changes. All reads retain existing server authorization/moderation.
|
|
5
|
+
* At most four requests per page; providers fail independently. */
|
|
6
|
+
export class DiscoveryFeedSession {
|
|
7
|
+
posts;
|
|
8
|
+
protocols;
|
|
9
|
+
category;
|
|
10
|
+
learning;
|
|
11
|
+
streams;
|
|
12
|
+
pending = null;
|
|
13
|
+
seen = new Set();
|
|
14
|
+
page = 0;
|
|
15
|
+
nextSource = 0;
|
|
16
|
+
constructor(posts, protocols, networks, category, source, learning) {
|
|
17
|
+
this.posts = posts;
|
|
18
|
+
this.protocols = protocols;
|
|
19
|
+
this.category = category;
|
|
20
|
+
this.learning = learning;
|
|
21
|
+
const sources = [...new Set(networks.filter(n => n.capabilities.includes('public_trends')).map(n => n.protocol))].slice(0, 16);
|
|
22
|
+
this.streams = ['minds', ...sources].filter(protocol => !source || source === 'all' || source === protocol)
|
|
23
|
+
.map(protocol => ({ protocol, offset: 0, done: false, failed: false, used: [] }));
|
|
24
|
+
}
|
|
25
|
+
retryFailed() { for (const stream of this.streams)
|
|
26
|
+
stream.failed = false; }
|
|
27
|
+
next() {
|
|
28
|
+
if (this.pending)
|
|
29
|
+
return this.pending;
|
|
30
|
+
this.pending = this.readPage().finally(() => { this.pending = null; });
|
|
31
|
+
return this.pending;
|
|
32
|
+
}
|
|
33
|
+
async read(stream) {
|
|
34
|
+
const params = { limit: 20, ...(this.category ? { category: this.category } : {}), discovery: true };
|
|
35
|
+
if (stream.protocol === 'minds') {
|
|
36
|
+
const response = await this.posts.list({ ...params, sort: 'new', ...(stream.cursor ? { cursor: stream.cursor } : { offset: stream.offset }) });
|
|
37
|
+
stream.offset += response.meta.scanned ?? response.meta.limit;
|
|
38
|
+
const next = response.meta.next_cursor ?? undefined;
|
|
39
|
+
stream.done = !response.meta.has_more || !!next && (next === stream.cursor || stream.used.includes(next));
|
|
40
|
+
if (stream.cursor)
|
|
41
|
+
stream.used = [...stream.used, stream.cursor].slice(-32);
|
|
42
|
+
stream.cursor = next;
|
|
43
|
+
return { items: response.data.map(post => ({ key: key('minds', post.id), kind: 'minds', post })),
|
|
44
|
+
layout: response.meta.discovery ?? ordinary, partial: response.meta.category_filter?.status === 'partial' };
|
|
45
|
+
}
|
|
46
|
+
const validatePosts = (data) => {
|
|
47
|
+
if (data.coverage !== 'provider_ranked_window' || data.items.length > 25 || data.items.some(post => post.protocol !== stream.protocol || post.author.protocol !== stream.protocol))
|
|
48
|
+
throw new Error('Invalid discovery source response');
|
|
49
|
+
};
|
|
50
|
+
if (stream.topics) {
|
|
51
|
+
const topic = stream.topics[0];
|
|
52
|
+
if (!topic) {
|
|
53
|
+
stream.done = true;
|
|
54
|
+
return { items: [], layout: ordinary, partial: false };
|
|
55
|
+
}
|
|
56
|
+
const { data } = await this.protocols.getPublicTrendFeed({ ...params, protocol: stream.protocol, topic: topic.topic, cursor: topic.cursor });
|
|
57
|
+
validatePosts(data);
|
|
58
|
+
if (data.trend.protocol !== stream.protocol || data.trend.nativeId !== topic.topic)
|
|
59
|
+
throw new Error('Invalid discovery topic');
|
|
60
|
+
const next = data.cursor;
|
|
61
|
+
stream.topics.shift();
|
|
62
|
+
if (next && next !== topic.cursor && !topic.used.includes(next))
|
|
63
|
+
stream.topics.push({ topic: topic.topic, cursor: next, used: [...topic.used, ...(topic.cursor ? [topic.cursor] : [])].slice(-32) });
|
|
64
|
+
stream.done = !stream.topics.length;
|
|
65
|
+
return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
|
|
66
|
+
}
|
|
67
|
+
const { data } = await this.protocols.getPublicTrends({ ...params, protocol: stream.protocol, cursor: stream.cursor, includePreviews: true });
|
|
68
|
+
if (data.coverage !== 'provider_ranked_window' || data.items.length > 25 || data.items.some(item => item.protocol !== stream.protocol))
|
|
69
|
+
throw new Error('Invalid discovery source response');
|
|
70
|
+
if (data.kind === 'topics') {
|
|
71
|
+
if (data.items.some(topic => topic.preview && (topic.preview.protocol !== stream.protocol || topic.preview.author.protocol !== stream.protocol)))
|
|
72
|
+
throw new Error('Invalid discovery preview');
|
|
73
|
+
stream.topics = [...new Set(data.items.map(topic => topic.nativeId))].map(topic => ({ topic, used: [] }));
|
|
74
|
+
stream.done = !stream.topics.length;
|
|
75
|
+
return { items: data.items.flatMap(topic => !this.category && topic.preview ? [{ key: key(topic.preview.protocol, topic.preview.nativeId), kind: 'social', post: topic.preview }] : []), layout: ordinary, partial: data.categoryFilter?.status === 'partial' };
|
|
76
|
+
}
|
|
77
|
+
validatePosts(data);
|
|
78
|
+
const next = data.cursor ?? undefined;
|
|
79
|
+
stream.done = !next || next === stream.cursor || stream.used.includes(next);
|
|
80
|
+
if (stream.cursor)
|
|
81
|
+
stream.used = [...stream.used, stream.cursor].slice(-32);
|
|
82
|
+
stream.cursor = next;
|
|
83
|
+
return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
|
|
84
|
+
}
|
|
85
|
+
async readPage() {
|
|
86
|
+
const active = this.streams.filter(stream => !stream.done && !stream.failed);
|
|
87
|
+
const native = active.find(stream => stream.protocol === 'minds');
|
|
88
|
+
const remote = active.filter(stream => stream.protocol !== 'minds');
|
|
89
|
+
const selected = [...(native ? [native] : []), ...Array.from({ length: Math.min(native ? 3 : 4, remote.length) }, (_, index) => remote[(this.nextSource + index) % remote.length])];
|
|
90
|
+
this.nextSource += 3;
|
|
91
|
+
const reads = await Promise.all(selected.map(async (stream) => {
|
|
92
|
+
// Commit continuation state only if this page finishes within its UI
|
|
93
|
+
// deadline. A late provider cannot move the cursor past unseen content.
|
|
94
|
+
const candidate = { ...stream, used: [...stream.used], topics: stream.topics?.map(topic => ({ ...topic, used: [...topic.used] })) };
|
|
95
|
+
let cancel = () => { };
|
|
96
|
+
try {
|
|
97
|
+
const read = await Promise.race([this.read(candidate), new Promise((_resolve, reject) => {
|
|
98
|
+
const timer = setTimeout(() => reject(new Error('Discovery source timed out')), 15000);
|
|
99
|
+
cancel = () => clearTimeout(timer);
|
|
100
|
+
})]);
|
|
101
|
+
Object.assign(stream, candidate);
|
|
102
|
+
return read;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
stream.failed = true;
|
|
106
|
+
return { items: [], layout: ordinary, partial: false };
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
cancel();
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
// Round-robin before grouping: the largest network cannot occupy every slot.
|
|
113
|
+
const mixed = [];
|
|
114
|
+
for (let index = 0; index < Math.max(0, ...reads.map(read => read.items.length)); index++)
|
|
115
|
+
for (const read of reads) {
|
|
116
|
+
const item = read.items[index];
|
|
117
|
+
if (!item || this.seen.has(item.key))
|
|
118
|
+
continue;
|
|
119
|
+
this.seen.add(item.key);
|
|
120
|
+
mixed.push({ item, layout: read.layout });
|
|
121
|
+
}
|
|
122
|
+
// Deduplication memory is bounded independently of the UI's virtual window.
|
|
123
|
+
while (this.seen.size > 4096)
|
|
124
|
+
this.seen.delete(this.seen.values().next().value);
|
|
125
|
+
const modules = [];
|
|
126
|
+
const page = this.page++;
|
|
127
|
+
for (let index = 0; index < mixed.length;) {
|
|
128
|
+
const first = mixed[index];
|
|
129
|
+
const proposal = first.layout.source === 'jev' ? first.layout.presentation : 'posts';
|
|
130
|
+
const preferred = this.learning?.choose(proposal, page) ?? proposal;
|
|
131
|
+
const group = mixed.slice(index, index + 4);
|
|
132
|
+
const video = preferred === 'video' && first.item.post.media?.some(media => media.type === 'video');
|
|
133
|
+
const visual = preferred === 'visual' && group.filter(row => row.item.post.media?.some(media => media.type === 'image')).length >= 2;
|
|
134
|
+
const discussion = preferred === 'conversation' && group.filter(row => row.item.kind === 'social' ? (row.item.post.replyCount ?? 0) > 0 : (row.item.post.reply_count ?? 0) > 0).length >= 2;
|
|
135
|
+
const presentation = video ? 'video' : visual ? 'visual' : discussion ? 'conversation' : 'posts';
|
|
136
|
+
const selected = presentation === 'posts' || video ? [first] : group;
|
|
137
|
+
const items = selected.map(row => row.item);
|
|
138
|
+
modules.push({ id: `discovery-${page}-${index}-${items[0].key}`, presentation, source: presentation === 'posts' ? 'fallback' : 'jev', items,
|
|
139
|
+
spec: { root: 'module', elements: { module: { type: video ? 'DiscoveryVideo' : visual ? 'DiscoveryGallery' : discussion ? 'DiscoveryConversation' : 'DiscoveryPosts', props: { itemKeys: items.map(item => item.key) }, children: [] } } } });
|
|
140
|
+
index += selected.length;
|
|
141
|
+
}
|
|
142
|
+
return { modules, hasMore: this.streams.some(stream => !stream.done && !stream.failed),
|
|
143
|
+
failedSources: this.streams.filter(stream => stream.failed).map(stream => stream.protocol), partialCategories: reads.some(read => read.partial) };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export class DiscoveryResource {
|
|
147
|
+
posts;
|
|
148
|
+
protocols;
|
|
149
|
+
constructor(posts, protocols) {
|
|
150
|
+
this.posts = posts;
|
|
151
|
+
this.protocols = protocols;
|
|
152
|
+
}
|
|
153
|
+
createFeed(options) {
|
|
154
|
+
return new DiscoveryFeedSession(this.posts, this.protocols, options.networks, options.category, options.source, options.learning);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DiscoveryLayout } from '../types.js';
|
|
1
2
|
import type { HttpClient } from '../client.js';
|
|
2
3
|
import type { PaginatedResponse, PaginationParams, SingleResponse } from '../types.js';
|
|
3
4
|
/** Canonical protocol operation names returned by the adapter registry. */
|
|
@@ -844,9 +845,10 @@ export interface SocialPost {
|
|
|
844
845
|
likeCount: number | null;
|
|
845
846
|
repostCount: number | null;
|
|
846
847
|
media: Array<{
|
|
847
|
-
type: 'image';
|
|
848
|
+
type: 'image' | 'video';
|
|
848
849
|
url: string;
|
|
849
850
|
alt: string;
|
|
851
|
+
poster?: string;
|
|
850
852
|
}>;
|
|
851
853
|
hasAdditionalContent: boolean;
|
|
852
854
|
/** At most one independently filtered quoted post. */
|
|
@@ -884,6 +886,7 @@ export interface SocialAuthorFeed {
|
|
|
884
886
|
coverage: 'original_posts';
|
|
885
887
|
}
|
|
886
888
|
export interface SocialTrendProvenance {
|
|
889
|
+
discovery?: DiscoveryLayout;
|
|
887
890
|
provider: {
|
|
888
891
|
name: string;
|
|
889
892
|
url: string;
|
|
@@ -1024,6 +1027,7 @@ export declare class ProtocolsResource {
|
|
|
1024
1027
|
cursor?: string;
|
|
1025
1028
|
includePreviews?: boolean;
|
|
1026
1029
|
category?: string;
|
|
1030
|
+
discovery?: boolean;
|
|
1027
1031
|
}): Promise<SingleResponse<SocialTrends>>;
|
|
1028
1032
|
/** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
|
|
1029
1033
|
getPublicTrendFeed(params: {
|
|
@@ -1032,6 +1036,7 @@ export declare class ProtocolsResource {
|
|
|
1032
1036
|
limit?: number;
|
|
1033
1037
|
cursor?: string;
|
|
1034
1038
|
category?: string;
|
|
1039
|
+
discovery?: boolean;
|
|
1035
1040
|
}): Promise<SingleResponse<SocialTrendFeed>>;
|
|
1036
1041
|
/** Provider-ranked public accounts with explicit provenance; served from the app's index when a fresh snapshot exists. */
|
|
1037
1042
|
getPublicPopularProfiles(params: {
|
|
@@ -111,12 +111,14 @@ export class ProtocolsResource {
|
|
|
111
111
|
getPublicTrends(params) {
|
|
112
112
|
return this.client.get('/protocols/social/trends', { protocol: params.protocol, limit: params.limit, cursor: params.cursor,
|
|
113
113
|
...(params.includePreviews !== undefined ? { includePreviews: params.includePreviews } : {}),
|
|
114
|
-
...(params.category !== undefined ? { category: params.category } : {})
|
|
114
|
+
...(params.category !== undefined ? { category: params.category } : {}),
|
|
115
|
+
...(params.discovery !== undefined ? { discovery: params.discovery } : {}) });
|
|
115
116
|
}
|
|
116
117
|
/** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
|
|
117
118
|
getPublicTrendFeed(params) {
|
|
118
119
|
return this.client.get('/protocols/social/trend-feed', { protocol: params.protocol, topic: params.topic, limit: params.limit, cursor: params.cursor,
|
|
119
|
-
...(params.category !== undefined ? { category: params.category } : {})
|
|
120
|
+
...(params.category !== undefined ? { category: params.category } : {}),
|
|
121
|
+
...(params.discovery !== undefined ? { discovery: params.discovery } : {}) });
|
|
120
122
|
}
|
|
121
123
|
/** Provider-ranked public accounts with explicit provenance; served from the app's index when a fresh snapshot exists. */
|
|
122
124
|
getPublicPopularProfiles(params) {
|
package/dist/types.d.ts
CHANGED
|
@@ -5,7 +5,20 @@ export interface PaginationParams {
|
|
|
5
5
|
cursor?: string;
|
|
6
6
|
[key: string]: unknown;
|
|
7
7
|
}
|
|
8
|
+
export interface DiscoveryLayout {
|
|
9
|
+
version: 'discovery-layout-v1';
|
|
10
|
+
presentation: 'posts' | 'visual' | 'conversation' | 'video';
|
|
11
|
+
source: 'jev' | 'fallback';
|
|
12
|
+
}
|
|
8
13
|
export interface PaginationMeta {
|
|
14
|
+
discovery?: DiscoveryLayout;
|
|
15
|
+
scanned?: number;
|
|
16
|
+
category_filter?: {
|
|
17
|
+
selected: string;
|
|
18
|
+
status: 'complete' | 'partial';
|
|
19
|
+
classified: number;
|
|
20
|
+
unclassified: number;
|
|
21
|
+
};
|
|
9
22
|
limit: number;
|
|
10
23
|
offset: number;
|
|
11
24
|
has_more: boolean;
|
|
@@ -93,6 +106,7 @@ export interface MediaItem {
|
|
|
93
106
|
type: string;
|
|
94
107
|
}
|
|
95
108
|
export interface Post {
|
|
109
|
+
reply_count?: number;
|
|
96
110
|
id: string;
|
|
97
111
|
/** Original Minds numeric GUID when this row came from the legacy import. */
|
|
98
112
|
legacy_guid?: string | null;
|
|
@@ -171,6 +185,8 @@ export interface UpdatePostInput {
|
|
|
171
185
|
}
|
|
172
186
|
export type ReactionType = 'like' | 'heart' | 'fire' | 'laugh' | 'sad' | 'angry' | 'upvote' | 'downvote';
|
|
173
187
|
export interface ListPostsParams extends PaginationParams {
|
|
188
|
+
/** Public-only feed with a bounded Jev presentation hint. Limit <=25. */
|
|
189
|
+
discovery?: boolean;
|
|
174
190
|
/** Public-only broad category. Requires an enabled app experiment; limit <=25. */
|
|
175
191
|
category?: string;
|
|
176
192
|
community_id?: string;
|