@meeovi/social 1.0.0 → 1.0.1

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 ADDED
@@ -0,0 +1,79 @@
1
+
2
+ ---
3
+
4
+ # 📦 `@meeovi/social` — README.md
5
+ *(This one is more advanced because of caching, SWR, retry, metrics, etc.)*
6
+
7
+ ```md
8
+ # @meeovi/social
9
+
10
+ A powerful, backend‑agnostic social integration layer for Meeovi.
11
+ Supports ATProto (Bluesky), Mastodon, WordPress/BuddyPress, and any custom social backend.
12
+
13
+ ## ✨ Features
14
+
15
+ - Pluggable social providers
16
+ - Unified `useSocial()` composable
17
+ - Unified activity feed abstraction
18
+ - Caching (TTL‑based)
19
+ - Stale‑While‑Revalidate (SWR) background refresh
20
+ - Retry + exponential backoff
21
+ - Provider‑specific rate‑limit strategies
22
+ - Analytics hooks (per‑provider metrics)
23
+ - Unified error model
24
+
25
+ ## 📦 Installation
26
+
27
+ ```sh
28
+ npm install @meeovi/social
29
+
30
+ 🧩 Usage
31
+
32
+ import { useSocial } from '@meeovi/social'
33
+
34
+ const { getActivityFeed } = useSocial()
35
+
36
+ const feed = await getActivityFeed('sebastian')
37
+ 🔌 Providers
38
+
39
+ export interface SocialProvider {
40
+ getProfile(handle: string): Promise<SocialProfile>
41
+ listPosts(handle: string): Promise<SocialPost[]>
42
+ createPost?(content: string): Promise<SocialPost>
43
+ }
44
+ Register:
45
+
46
+
47
+ registerSocialProvider('mastodon', MastodonProvider)
48
+ 🧠 Advanced Features
49
+ Caching
50
+
51
+ cacheKey: `atproto:posts:${handle}`,
52
+ ttlMs: 30000
53
+ SWR (Stale‑While‑Revalidate)
54
+
55
+ swr: true
56
+ Retry + Backoff
57
+
58
+ retry: true
59
+ Provider‑Specific Rate Limits
60
+
61
+ setRateLimit('mastodon', { maxRequests: 10, windowMs: 60000 })
62
+ Metrics
63
+
64
+ import { getMetrics } from '@meeovi/social'
65
+
66
+ console.log(getMetrics('atproto'))
67
+ 🧱 Folder Structure
68
+ Code
69
+ src/
70
+ providers/
71
+ config.
72
+ registry.
73
+ useSocial.
74
+ cache.
75
+ retry.
76
+ swr.
77
+ rateLimit.
78
+ metrics.
79
+ utils.
package/index.ts CHANGED
@@ -0,0 +1,8 @@
1
+ export * from './src/types'
2
+ export * from './src/config'
3
+ export * from './src/registry'
4
+ export * from './src/useSocial'
5
+
6
+ // providers auto‑register on import
7
+ export * from './src/providers/atproto'
8
+ export * from './src/providers/mastodon'
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@meeovi/social",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
+ "exports": { ".": "./index.ts" },
6
7
  "scripts": {
7
8
  "test": "echo \"Error: no test specified\" && exit 1"
8
9
  },
package/src/cache.ts ADDED
@@ -0,0 +1,44 @@
1
+ export interface CacheEntry<T> {
2
+ value: T
3
+ expiresAt: number
4
+ }
5
+
6
+ export interface SocialCache {
7
+ store: any
8
+ get<T>(key: string): T | null
9
+ set<T>(key: string, value: T, ttlMs: number): void
10
+ delete(key: string): void
11
+ }
12
+
13
+ let cache: SocialCache = {
14
+ store: new Map<string, CacheEntry<any>>(),
15
+
16
+ get<T>(key: string): T | null {
17
+ const entry = this.store.get(key)
18
+ if (!entry) return null
19
+ if (Date.now() > entry.expiresAt) {
20
+ this.store.delete(key)
21
+ return null
22
+ }
23
+ return entry.value
24
+ },
25
+
26
+ set<T>(key: string, value: T, ttlMs: number) {
27
+ this.store.set(key, {
28
+ value,
29
+ expiresAt: Date.now() + ttlMs
30
+ })
31
+ },
32
+
33
+ delete(key: string) {
34
+ this.store.delete(key)
35
+ }
36
+ }
37
+
38
+ export function setSocialCache(customCache: SocialCache) {
39
+ cache = customCache
40
+ }
41
+
42
+ export function getSocialCache() {
43
+ return cache
44
+ }
package/src/config.ts CHANGED
@@ -1,27 +1,18 @@
1
-
2
1
  export interface SocialConfig {
3
- providers: {
4
- google?: boolean
5
- github?: boolean
6
- facebook?: boolean
7
- twitter?: boolean
8
- apple?: boolean
9
- [key: string]: any
10
- }
11
- redirectUrl?: string
2
+ provider: string
3
+ baseUrl?: string
4
+ apiKey?: string
5
+ [key: string]: any
12
6
  }
13
7
 
14
8
  let config: SocialConfig = {
15
- providers: {},
16
- redirectUrl: ''
9
+ provider: 'atproto',
10
+ baseUrl: '',
11
+ apiKey: ''
17
12
  }
18
13
 
19
14
  export function setSocialConfig(newConfig: Partial<SocialConfig>) {
20
- config = {
21
- ...config,
22
- providers: { ...config.providers, ...(newConfig.providers || {}) },
23
- redirectUrl: newConfig.redirectUrl ?? config.redirectUrl
24
- }
15
+ config = { ...config, ...newConfig }
25
16
  }
26
17
 
27
18
  export function getSocialConfig(): SocialConfig {
package/src/errors.ts ADDED
@@ -0,0 +1,16 @@
1
+ export class SocialError extends Error {
2
+ constructor(message: string, public provider?: string, public cause?: any) {
3
+ super(message)
4
+ this.name = 'SocialError'
5
+ }
6
+ }
7
+
8
+ export class RateLimitError extends SocialError {
9
+ retryAfter?: number
10
+
11
+ constructor(message: string, provider?: string, retryAfter?: number, cause?: any) {
12
+ super(message, provider, cause)
13
+ this.name = 'RateLimitError'
14
+ this.retryAfter = retryAfter
15
+ }
16
+ }
package/src/metrics.ts ADDED
@@ -0,0 +1,52 @@
1
+ export interface SocialMetrics {
2
+ requests: number
3
+ successes: number
4
+ failures: number
5
+ cacheHits: number
6
+ cacheMisses: number
7
+ backgroundRefreshes: number
8
+ }
9
+
10
+ const metrics = new Map<string, SocialMetrics>()
11
+
12
+ function getProviderMetrics(provider: string): SocialMetrics {
13
+ if (!metrics.has(provider)) {
14
+ metrics.set(provider, {
15
+ requests: 0,
16
+ successes: 0,
17
+ failures: 0,
18
+ cacheHits: 0,
19
+ cacheMisses: 0,
20
+ backgroundRefreshes: 0
21
+ })
22
+ }
23
+ return metrics.get(provider)!
24
+ }
25
+
26
+ export function recordRequest(provider: string) {
27
+ getProviderMetrics(provider).requests++
28
+ }
29
+
30
+ export function recordSuccess(provider: string) {
31
+ getProviderMetrics(provider).successes++
32
+ }
33
+
34
+ export function recordFailure(provider: string) {
35
+ getProviderMetrics(provider).failures++
36
+ }
37
+
38
+ export function recordCacheHit(provider: string) {
39
+ getProviderMetrics(provider).cacheHits++
40
+ }
41
+
42
+ export function recordCacheMiss(provider: string) {
43
+ getProviderMetrics(provider).cacheMisses++
44
+ }
45
+
46
+ export function recordBackgroundRefresh(provider: string) {
47
+ getProviderMetrics(provider).backgroundRefreshes++
48
+ }
49
+
50
+ export function getMetrics(provider: string) {
51
+ return getProviderMetrics(provider)
52
+ }
@@ -0,0 +1,75 @@
1
+ import { wrapSocialRequest } from '../utils'
2
+ import { getSocialConfig } from '../config'
3
+ import { registerSocialProvider } from '../registry'
4
+
5
+ async function atprotoFetch(path: string, options: RequestInit = {}) {
6
+ const { baseUrl, apiKey } = getSocialConfig()
7
+ const res = await fetch(`${baseUrl}${path}`, {
8
+ ...options,
9
+ headers: {
10
+ 'Content-Type': 'application/json',
11
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
12
+ ...(options.headers || {})
13
+ }
14
+ })
15
+
16
+ if (!res.ok) {
17
+ const error: any = new Error(`ATProto error: ${res.status}`)
18
+ error.status = res.status
19
+ error.response = res
20
+ throw error
21
+ }
22
+
23
+ return res.json()
24
+ }
25
+
26
+ const AtprotoProvider = {
27
+ async getProfile(handle: string) {
28
+ return wrapSocialRequest(
29
+ 'atproto',
30
+ async () => {
31
+ const data = await atprotoFetch(`/xrpc/app.bsky.actor.getProfile?actor=${handle}`)
32
+ return {
33
+ id: data.did,
34
+ username: data.handle,
35
+ displayName: data.displayName,
36
+ avatarUrl: data.avatar,
37
+ url: `https://bsky.app/profile/${data.handle}`
38
+ }
39
+ },
40
+ {
41
+ cacheKey: `atproto:profile:${handle}`,
42
+ ttlMs: 1000 * 60 * 5, // 5 minutes
43
+ retry: true
44
+ }
45
+ )
46
+ },
47
+
48
+ async listPosts(handle: string) {
49
+ return wrapSocialRequest(
50
+ 'atproto',
51
+ async () => {
52
+ const data = await atprotoFetch(`/xrpc/app.bsky.feed.getAuthorFeed?actor=${handle}`)
53
+ return (data.feed || []).map((item: any) => ({
54
+ id: item.post.uri,
55
+ content: item.post.record?.text,
56
+ createdAt: item.post.record?.createdAt,
57
+ author: {
58
+ id: item.post.author.did,
59
+ username: item.post.author.handle,
60
+ displayName: item.post.author.displayName,
61
+ avatarUrl: item.post.author.avatar
62
+ },
63
+ url: `https://bsky.app/profile/${item.post.author.handle}/post/${item.post.uri}`
64
+ }))
65
+ },
66
+ {
67
+ cacheKey: `atproto:posts:${handle}`,
68
+ ttlMs: 1000 * 30, // 30 seconds
69
+ retry: true
70
+ }
71
+ )
72
+ }
73
+ }
74
+
75
+ registerSocialProvider('atproto', AtprotoProvider)
@@ -0,0 +1,45 @@
1
+ // src/providers/mastodon.ts
2
+ import type { SocialProvider, SocialProfile, SocialPost } from '../types'
3
+ import { registerSocialProvider } from '../registry'
4
+ import { getSocialConfig } from '../config'
5
+
6
+ async function mastodonFetch(path: string, options: RequestInit = {}) {
7
+ const { baseUrl, apiKey } = getSocialConfig()
8
+ const res = await fetch(`${baseUrl}${path}`, {
9
+ ...options,
10
+ headers: {
11
+ 'Content-Type': 'application/json',
12
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
13
+ ...(options.headers || {})
14
+ }
15
+ })
16
+ if (!res.ok) throw new Error(`Mastodon error: ${res.status}`)
17
+ return res.json()
18
+ }
19
+
20
+ const MastodonProvider: SocialProvider = {
21
+ async getProfile(handle: string): Promise<SocialProfile> {
22
+ const data = await mastodonFetch(`/api/v1/accounts/lookup?acct=${handle}`)
23
+ return {
24
+ id: data.id,
25
+ username: data.acct,
26
+ displayName: data.display_name,
27
+ avatarUrl: data.avatar,
28
+ url: data.url
29
+ }
30
+ },
31
+
32
+ async listPosts(handle: string): Promise<SocialPost[]> {
33
+ const profile = await this.getProfile(handle)
34
+ const data = await mastodonFetch(`/api/v1/accounts/${profile.id}/statuses`)
35
+ return data.map((status: any) => ({
36
+ id: status.id,
37
+ content: status.content,
38
+ createdAt: status.created_at,
39
+ author: profile,
40
+ url: status.url
41
+ }))
42
+ }
43
+ }
44
+
45
+ registerSocialProvider('mastodon', MastodonProvider)
@@ -0,0 +1,41 @@
1
+ import { RateLimitError } from "./errors";
2
+
3
+ export interface RateLimitConfig {
4
+ maxRequests: number
5
+ windowMs: number
6
+ }
7
+
8
+ const providerLimits = new Map<string, RateLimitConfig>()
9
+ const providerUsage = new Map<string, { count: number; resetAt: number }>()
10
+
11
+ export function setRateLimit(provider: string, config: RateLimitConfig) {
12
+ providerLimits.set(provider, config)
13
+ }
14
+
15
+ export function checkRateLimit(provider: string) {
16
+ const limit = providerLimits.get(provider)
17
+ if (!limit) return
18
+
19
+ const now = Date.now()
20
+ const usage = providerUsage.get(provider) || {
21
+ count: 0,
22
+ resetAt: now + limit.windowMs
23
+ }
24
+
25
+ if (now > usage.resetAt) {
26
+ usage.count = 0
27
+ usage.resetAt = now + limit.windowMs
28
+ }
29
+
30
+ usage.count++
31
+
32
+ providerUsage.set(provider, usage)
33
+
34
+ if (usage.count > limit.maxRequests) {
35
+ throw new RateLimitError(
36
+ `Meeovi internal rate limit exceeded for provider "${provider}"`,
37
+ provider,
38
+ Math.ceil((usage.resetAt - now) / 1000)
39
+ )
40
+ }
41
+ }
@@ -0,0 +1,13 @@
1
+ import type { SocialProvider } from './types'
2
+
3
+ const providers: Record<string, SocialProvider> = {}
4
+
5
+ export function registerSocialProvider(name: string, provider: SocialProvider) {
6
+ providers[name] = provider
7
+ }
8
+
9
+ export function getSocialProvider(name: string): SocialProvider {
10
+ const provider = providers[name]
11
+ if (!provider) throw new Error(`Social provider "${name}" not found`)
12
+ return provider
13
+ }
package/src/retry.ts ADDED
@@ -0,0 +1,40 @@
1
+ export interface RetryOptions {
2
+ retries: number
3
+ baseDelayMs: number
4
+ maxDelayMs: number
5
+ }
6
+
7
+ const defaultRetry: RetryOptions = {
8
+ retries: 3,
9
+ baseDelayMs: 300,
10
+ maxDelayMs: 5000
11
+ }
12
+
13
+ function sleep(ms: number) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms))
15
+ }
16
+
17
+ export async function withRetry<T>(
18
+ fn: () => Promise<T>,
19
+ opts: Partial<RetryOptions> = {}
20
+ ): Promise<T> {
21
+ const { retries, baseDelayMs, maxDelayMs } = { ...defaultRetry, ...opts }
22
+
23
+ let attempt = 0
24
+
25
+ while (true) {
26
+ try {
27
+ return await fn()
28
+ } catch (err: any) {
29
+ attempt++
30
+ if (attempt > retries) throw err
31
+
32
+ const delay = Math.min(
33
+ baseDelayMs * Math.pow(2, attempt) + Math.random() * 100,
34
+ maxDelayMs
35
+ )
36
+
37
+ await sleep(delay)
38
+ }
39
+ }
40
+ }
package/src/swr.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { getSocialCache } from './cache'
2
+
3
+ const refreshQueue = new Map<string, Promise<any>>()
4
+
5
+ export function scheduleBackgroundRefresh(
6
+ key: string,
7
+ fn: () => Promise<any>,
8
+ ttlMs: number
9
+ ) {
10
+ if (refreshQueue.has(key)) return
11
+
12
+ const promise = fn()
13
+ .then((fresh) => {
14
+ const cache = getSocialCache()
15
+ cache.set(key, fresh, ttlMs)
16
+ })
17
+ .finally(() => {
18
+ refreshQueue.delete(key)
19
+ })
20
+
21
+ refreshQueue.set(key, promise)
22
+ }
package/src/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ export interface SocialProfile {
2
+ id: string
3
+ username: string
4
+ displayName?: string
5
+ avatarUrl?: string
6
+ url?: string
7
+ [key: string]: any
8
+ }
9
+
10
+ export interface SocialPost {
11
+ id: string
12
+ content: string
13
+ createdAt: string
14
+ author: SocialProfile
15
+ url?: string
16
+ [key: string]: any
17
+ }
18
+
19
+ export interface SocialActivity extends SocialPost {
20
+ provider: string
21
+ source?: string
22
+ }
23
+
24
+ export interface SocialProvider {
25
+ getProfile(handle: string): Promise<SocialProfile>
26
+ listPosts(handle: string, options?: Record<string, any>): Promise<SocialPost[]>
27
+ createPost?(content: string, options?: Record<string, any>): Promise<SocialPost>
28
+ }
@@ -0,0 +1,25 @@
1
+ import { getSocialConfig } from './config'
2
+ import { getSocialProvider } from './registry'
3
+ import type { SocialActivity } from './types'
4
+
5
+ export function useSocial() {
6
+ const { provider } = getSocialConfig()
7
+ const socialProvider = getSocialProvider(provider)
8
+
9
+ async function getActivityFeed(handle: string, options?: Record<string, any>): Promise<SocialActivity[]> {
10
+ const posts = await socialProvider.listPosts(handle, options)
11
+
12
+ return posts.map((post) => ({
13
+ ...post,
14
+ provider,
15
+ source: provider
16
+ }))
17
+ }
18
+
19
+ return {
20
+ getProfile: socialProvider.getProfile,
21
+ listPosts: socialProvider.listPosts,
22
+ createPost: socialProvider.createPost,
23
+ getActivityFeed
24
+ }
25
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,99 @@
1
+ // utils.ts
2
+
3
+ import { RateLimitError, SocialError } from './errors'
4
+ import { getSocialCache } from './cache'
5
+ import { withRetry } from './retry'
6
+ import { scheduleBackgroundRefresh } from './swr'
7
+ import { checkRateLimit } from './rateLimit'
8
+ import {
9
+ recordRequest,
10
+ recordSuccess,
11
+ recordFailure,
12
+ recordCacheHit,
13
+ recordCacheMiss
14
+ } from './metrics'
15
+
16
+ export interface RequestOptions {
17
+ cacheKey?: string
18
+ ttlMs?: number
19
+ retry?: boolean
20
+ swr?: boolean
21
+ }
22
+
23
+ /**
24
+ * Core wrapper for all provider requests.
25
+ * Handles:
26
+ * - caching
27
+ * - SWR background refresh
28
+ * - retry/backoff
29
+ * - provider-specific rate limits
30
+ * - unified error handling
31
+ * - analytics hooks
32
+ */
33
+ export async function wrapSocialRequest<T>(
34
+ provider: string,
35
+ fn: () => Promise<T>,
36
+ opts: RequestOptions = {}
37
+ ): Promise<T> {
38
+ const cache = getSocialCache()
39
+
40
+ // Track request attempt
41
+ recordRequest(provider)
42
+
43
+ // Provider-specific rate limit enforcement
44
+ checkRateLimit(provider)
45
+
46
+ // 1. Cache check
47
+ if (opts.cacheKey) {
48
+ const cached = cache.get<T>(opts.cacheKey)
49
+
50
+ if (cached) {
51
+ recordCacheHit(provider)
52
+
53
+ // SWR: return cached immediately, refresh in background
54
+ if (opts.swr && opts.ttlMs) {
55
+ scheduleBackgroundRefresh(opts.cacheKey, fn, opts.ttlMs)
56
+ }
57
+
58
+ return cached
59
+ }
60
+
61
+ recordCacheMiss(provider)
62
+ }
63
+
64
+ // 2. Execute request (with optional retry/backoff)
65
+ let result: T
66
+
67
+ try {
68
+ result = await (opts.retry ? withRetry(fn) : fn())
69
+ recordSuccess(provider)
70
+ } catch (err: any) {
71
+ recordFailure(provider)
72
+
73
+ const status = err?.status || err?.response?.status
74
+ const retryAfterHeader = err?.response?.headers?.['retry-after']
75
+ const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : undefined
76
+
77
+ if (status === 429) {
78
+ throw new RateLimitError(
79
+ `Rate limit exceeded for provider "${provider}"`,
80
+ provider,
81
+ retryAfter,
82
+ err
83
+ )
84
+ }
85
+
86
+ throw new SocialError(
87
+ `Social provider "${provider}" request failed`,
88
+ provider,
89
+ err
90
+ )
91
+ }
92
+
93
+ // 3. Cache fresh result
94
+ if (opts.cacheKey && opts.ttlMs) {
95
+ cache.set(opts.cacheKey, result, opts.ttlMs)
96
+ }
97
+
98
+ return result
99
+ }