@weifuwujs/messages 0.0.0-stage → 1.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.
Files changed (63) hide show
  1. package/README.md +259 -2
  2. package/dist/bus.d.ts +7 -0
  3. package/dist/bus.d.ts.map +1 -0
  4. package/dist/bus.js +30 -0
  5. package/dist/bus.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +82 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/cursor.d.ts +20 -0
  11. package/dist/cursor.d.ts.map +1 -0
  12. package/dist/cursor.js +49 -0
  13. package/dist/cursor.js.map +1 -0
  14. package/dist/hub.d.ts +15 -0
  15. package/dist/hub.d.ts.map +1 -0
  16. package/dist/hub.js +51 -0
  17. package/dist/hub.js.map +1 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +5 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/messages.d.ts +10 -0
  23. package/dist/messages.d.ts.map +1 -0
  24. package/dist/messages.js +272 -0
  25. package/dist/messages.js.map +1 -0
  26. package/dist/postgres.d.ts +3 -0
  27. package/dist/postgres.d.ts.map +1 -0
  28. package/dist/postgres.js +2 -0
  29. package/dist/postgres.js.map +1 -0
  30. package/dist/redis.d.ts +24 -0
  31. package/dist/redis.d.ts.map +1 -0
  32. package/dist/redis.js +126 -0
  33. package/dist/redis.js.map +1 -0
  34. package/dist/sqlite.d.ts +3 -0
  35. package/dist/sqlite.d.ts.map +1 -0
  36. package/dist/sqlite.js +2 -0
  37. package/dist/sqlite.js.map +1 -0
  38. package/dist/stores/memory.d.ts +6 -0
  39. package/dist/stores/memory.d.ts.map +1 -0
  40. package/dist/stores/memory.js +115 -0
  41. package/dist/stores/memory.js.map +1 -0
  42. package/dist/stores/postgres.d.ts +22 -0
  43. package/dist/stores/postgres.d.ts.map +1 -0
  44. package/dist/stores/postgres.js +192 -0
  45. package/dist/stores/postgres.js.map +1 -0
  46. package/dist/stores/sqlite.d.ts +13 -0
  47. package/dist/stores/sqlite.d.ts.map +1 -0
  48. package/dist/stores/sqlite.js +180 -0
  49. package/dist/stores/sqlite.js.map +1 -0
  50. package/dist/testing.d.ts +24 -0
  51. package/dist/testing.d.ts.map +1 -0
  52. package/dist/testing.js +248 -0
  53. package/dist/testing.js.map +1 -0
  54. package/dist/types.d.ts +171 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +2 -0
  57. package/dist/types.js.map +1 -0
  58. package/package.json +64 -4
  59. package/templates/react-router-messages/app/messages.ts +13 -0
  60. package/templates/react-router-messages/app/routes/home.tsx +51 -0
  61. package/templates/react-router-messages/app/routes/notifications.tsx +91 -0
  62. package/templates/react-router-messages/app/routes.ts +10 -0
  63. package/templates/react-router-messages/app/server.ts +41 -0
@@ -0,0 +1,91 @@
1
+ import { useEffect } from 'react'
2
+ import { Form, redirect, useRevalidator } from 'react-router'
3
+ import type { Route } from './+types/notifications'
4
+ import { authUserContext } from '~/context.ts'
5
+ import { messages } from '~/messages.ts'
6
+
7
+ export function meta() {
8
+ return [{ title: '消息' }]
9
+ }
10
+
11
+ export async function loader({ context }: Route.LoaderArgs) {
12
+ const user = context.get(authUserContext)
13
+ if (!user) throw redirect('/login')
14
+ const page = await messages.list({ userId: user.id, limit: 20 })
15
+ return {
16
+ unread: await messages.unread(user.id),
17
+ nextCursor: page.nextCursor,
18
+ items: page.items.map((message) => ({
19
+ id: message.id,
20
+ kind: message.kind,
21
+ title: message.title,
22
+ body: message.body,
23
+ readAt: message.readAt === null ? null : message.readAt.toISOString(),
24
+ createdAt: message.createdAt.toISOString(),
25
+ })),
26
+ }
27
+ }
28
+
29
+ export async function action({ context }: Route.ActionArgs) {
30
+ const user = context.get(authUserContext)
31
+ if (!user) throw redirect('/login')
32
+ await messages.markAllRead(user.id)
33
+ return redirect('/notifications')
34
+ }
35
+
36
+ /** 实时:服务端经 /messages/ws 推送 message / unread,收到即重新验证 loader */
37
+ function useRealtime(): void {
38
+ const revalidator = useRevalidator()
39
+ useEffect(() => {
40
+ const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
41
+ const socket = new WebSocket(`${protocol}://${location.host}/messages/ws`)
42
+ socket.addEventListener('message', (event) => {
43
+ const data = JSON.parse(String(event.data)) as { type?: string }
44
+ if (data.type === 'message' || data.type === 'unread') {
45
+ revalidator.revalidate()
46
+ }
47
+ })
48
+ return () => socket.close()
49
+ }, [revalidator])
50
+ }
51
+
52
+ export default function Notifications({ loaderData }: Route.ComponentProps) {
53
+ const { items, unread } = loaderData
54
+ useRealtime()
55
+
56
+ return (
57
+ <main className="mx-auto max-w-xl p-8">
58
+ <div className="flex items-center justify-between">
59
+ <h1 className="text-2xl font-bold">
60
+ 消息{unread > 0 ? `(未读 ${unread})` : ''}
61
+ </h1>
62
+ {unread > 0 ? (
63
+ <Form method="post">
64
+ <button className="border px-3 py-1.5 text-sm">全部已读</button>
65
+ </Form>
66
+ ) : null}
67
+ </div>
68
+
69
+ <ul className="mt-6 space-y-3">
70
+ {items.map((message) => (
71
+ <li
72
+ key={message.id}
73
+ data-read={message.readAt === null ? 'false' : 'true'}
74
+ className="border p-3"
75
+ >
76
+ <div className="font-semibold">{message.title}</div>
77
+ {message.body ? (
78
+ <p className="mt-1 text-sm text-gray-600">{message.body}</p>
79
+ ) : null}
80
+ <div className="mt-1 text-xs text-gray-400">
81
+ {message.kind} · {message.createdAt}
82
+ </div>
83
+ </li>
84
+ ))}
85
+ {items.length === 0 ? (
86
+ <li className="border p-3 text-sm text-gray-500">暂无消息</li>
87
+ ) : null}
88
+ </ul>
89
+ </main>
90
+ )
91
+ }
@@ -0,0 +1,10 @@
1
+ import { index, route, type RouteConfig } from '@react-router/dev/routes'
2
+
3
+ export default [
4
+ index('routes/home.tsx'),
5
+ route('login', 'routes/login.tsx'),
6
+ route('register', 'routes/register.tsx'),
7
+ route('reset-password', 'routes/reset-password.tsx'),
8
+ route('account', 'routes/account.tsx'),
9
+ route('notifications', 'routes/notifications.tsx'),
10
+ ] satisfies RouteConfig
@@ -0,0 +1,41 @@
1
+ import { createApp, json } from '@weifuwujs/weifuwu'
2
+ import { reactRouter } from '@weifuwujs/weifuwu/react-router'
3
+ import type { Context, Middleware } from '@weifuwujs/weifuwu'
4
+ import { RouterContextProvider } from 'react-router'
5
+ import { authUserContext, greetingContext } from './context.ts'
6
+ import { auth } from './auth.ts'
7
+ import { messages } from './messages.ts'
8
+
9
+ type AppContext = Context & {
10
+ greeting: string
11
+ auth?: import('@weifuwujs/weifuwu/auth').AuthContext
12
+ }
13
+
14
+ const withGreeting: Middleware<Context, AppContext> = async (
15
+ request,
16
+ context,
17
+ next,
18
+ ) => next(request, { ...context, greeting: 'hello-from-weifuwu' })
19
+
20
+ const app = createApp().use(withGreeting)
21
+
22
+ // HTTP + WS 双端中间件:WS 升级也要认会话 cookie(messages 端点鉴权依赖)
23
+ auth.install(app)
24
+ app.mount(auth)
25
+ app.use('/account', auth.require())
26
+ // 一条线:REST(/messages 只读端点)+ WS(/messages/ws,未认证 401)
27
+ messages.install(app)
28
+ app.get('/api/health', () => json({ ok: true }))
29
+
30
+ app.notFound(
31
+ reactRouter<AppContext>({
32
+ getLoadContext(_request, context) {
33
+ const loadContext = new RouterContextProvider()
34
+ loadContext.set(greetingContext, context.greeting)
35
+ loadContext.set(authUserContext, context.auth?.user ?? null)
36
+ return loadContext
37
+ },
38
+ }),
39
+ )
40
+
41
+ export default app