@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.
- package/README.md +259 -2
- package/dist/bus.d.ts +7 -0
- package/dist/bus.d.ts.map +1 -0
- package/dist/bus.js +30 -0
- package/dist/bus.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +82 -0
- package/dist/cli.js.map +1 -0
- package/dist/cursor.d.ts +20 -0
- package/dist/cursor.d.ts.map +1 -0
- package/dist/cursor.js +49 -0
- package/dist/cursor.js.map +1 -0
- package/dist/hub.d.ts +15 -0
- package/dist/hub.d.ts.map +1 -0
- package/dist/hub.js +51 -0
- package/dist/hub.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +10 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +272 -0
- package/dist/messages.js.map +1 -0
- package/dist/postgres.d.ts +3 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +2 -0
- package/dist/postgres.js.map +1 -0
- package/dist/redis.d.ts +24 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +126 -0
- package/dist/redis.js.map +1 -0
- package/dist/sqlite.d.ts +3 -0
- package/dist/sqlite.d.ts.map +1 -0
- package/dist/sqlite.js +2 -0
- package/dist/sqlite.js.map +1 -0
- package/dist/stores/memory.d.ts +6 -0
- package/dist/stores/memory.d.ts.map +1 -0
- package/dist/stores/memory.js +115 -0
- package/dist/stores/memory.js.map +1 -0
- package/dist/stores/postgres.d.ts +22 -0
- package/dist/stores/postgres.d.ts.map +1 -0
- package/dist/stores/postgres.js +192 -0
- package/dist/stores/postgres.js.map +1 -0
- package/dist/stores/sqlite.d.ts +13 -0
- package/dist/stores/sqlite.d.ts.map +1 -0
- package/dist/stores/sqlite.js +180 -0
- package/dist/stores/sqlite.js.map +1 -0
- package/dist/testing.d.ts +24 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +248 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +171 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -4
- package/templates/react-router-messages/app/messages.ts +13 -0
- package/templates/react-router-messages/app/routes/home.tsx +51 -0
- package/templates/react-router-messages/app/routes/notifications.tsx +91 -0
- package/templates/react-router-messages/app/routes.ts +10 -0
- package/templates/react-router-messages/app/server.ts +41 -0
package/README.md
CHANGED
|
@@ -1,3 +1,260 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 站内信(`@weifuwujs/messages`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
用户通知的最小完整形态:**服务端发送(幂等)** + **REST 收件箱** + **WebSocket 实时推送** +
|
|
4
|
+
**断线 `since` 补投**,memory / sqlite / postgres 三驱动共用同一套契约测试。适合站内信、系统通知、
|
|
5
|
+
"任务完成"提醒这类"写入一次、本人可查、要实时"的场景。
|
|
6
|
+
|
|
7
|
+
不适合的:IM 聊天(双向会话、打字状态、已读回执)、服务间消息总线(用 [`@weifuwujs/tasks`](./packages/tasks/README.md) 或
|
|
8
|
+
MQ)、给非登录用户群发(消息挂在用户上,先有账号)。
|
|
9
|
+
|
|
10
|
+
## 十分钟接入
|
|
11
|
+
|
|
12
|
+
最快的路径是脚手架 + 包内叠加模板:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @weifuwujs/weifuwu init my-app
|
|
16
|
+
cd my-app
|
|
17
|
+
cp -r "$(npm root)/@weifuwujs/auth/templates/react-router-auth/." .
|
|
18
|
+
cp -r "$(npm root)/@weifuwujs/messages/templates/react-router-messages/." .
|
|
19
|
+
npx @weifuwujs/auth migrate
|
|
20
|
+
npx @weifuwujs/messages migrate # 欢迎信 + 徽标 + /notifications 页 + WS 实时
|
|
21
|
+
npm run dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
手工接入只有四步(**选驱动 → `createMessages` → `install(app)` → `send`**):
|
|
25
|
+
|
|
26
|
+
```ts messages
|
|
27
|
+
import { createApp } from '@weifuwujs/weifuwu'
|
|
28
|
+
import { createMessages } from '@weifuwujs/messages'
|
|
29
|
+
import { sqliteMessageStore } from '@weifuwujs/messages/sqlite'
|
|
30
|
+
|
|
31
|
+
const messages = createMessages({
|
|
32
|
+
store: sqliteMessageStore('data/messages.db'), // 构造即迁移(幂等)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const app = createApp()
|
|
36
|
+
messages.install(app) // REST + WebSocket 一条线(都要求登录,未认证 401),并随 app.close() 收连接
|
|
37
|
+
|
|
38
|
+
await messages.send({
|
|
39
|
+
userId: 'user-1',
|
|
40
|
+
kind: 'welcome',
|
|
41
|
+
title: '欢迎使用',
|
|
42
|
+
body: '从这条消息开始。',
|
|
43
|
+
idempotencyKey: 'welcome:user-1', // 重复发送只入库一次,不覆盖
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`send` 是**服务端 API,不暴露 HTTP**(通知面不能被客户端伪造);消息落库后在线的本人立刻收到 WS 推送。
|
|
48
|
+
|
|
49
|
+
配合 auth 的注册欢迎信 recipe:
|
|
50
|
+
|
|
51
|
+
```ts messages-auth
|
|
52
|
+
import { createApp } from '@weifuwujs/weifuwu'
|
|
53
|
+
import { createAuth } from '@weifuwujs/auth'
|
|
54
|
+
import { sqliteAuthStore } from '@weifuwujs/auth/sqlite'
|
|
55
|
+
import { createMessages } from '@weifuwujs/messages'
|
|
56
|
+
import { sqliteMessageStore } from '@weifuwujs/messages/sqlite'
|
|
57
|
+
|
|
58
|
+
const app = createApp()
|
|
59
|
+
const messages = createMessages({ store: sqliteMessageStore('data/messages.db') })
|
|
60
|
+
const auth = createAuth({
|
|
61
|
+
store: sqliteAuthStore('data/auth.db'),
|
|
62
|
+
hooks: {
|
|
63
|
+
onRegister: (user) => {
|
|
64
|
+
void messages.send({
|
|
65
|
+
userId: user.id,
|
|
66
|
+
kind: 'welcome',
|
|
67
|
+
title: '欢迎使用',
|
|
68
|
+
idempotencyKey: `welcome:${user.id}`, // 重投/重试只入一条
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
auth.install(app) // HTTP + WS 统一鉴权(ctx.auth.user)
|
|
75
|
+
app.mount(auth)
|
|
76
|
+
messages.install(app) // 必须叠在 auth 之后:REST/WS 都看 ctx.auth.user
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 后台任务完成通知(tasks recipe)
|
|
80
|
+
|
|
81
|
+
长任务在 [`./tasks`](./tasks.md) 里跑完,用消息把结果送到用户手里;`idempotencyKey` 按业务主键命名,
|
|
82
|
+
任务重试(至少一次执行)也不会重复打扰用户:
|
|
83
|
+
|
|
84
|
+
```ts messages-tasks
|
|
85
|
+
import { createApp } from '@weifuwujs/weifuwu'
|
|
86
|
+
import { createMessages, memoryMessageStore } from '@weifuwujs/messages'
|
|
87
|
+
import { createTasks, memoryTaskStore } from '@weifuwujs/tasks'
|
|
88
|
+
|
|
89
|
+
const messages = createMessages({ store: memoryMessageStore() })
|
|
90
|
+
const tasks = createTasks({ store: memoryTaskStore() })
|
|
91
|
+
|
|
92
|
+
tasks.handle(
|
|
93
|
+
'generate-report',
|
|
94
|
+
async (payload: { userId: string; reportId: string }) => {
|
|
95
|
+
await messages.send({
|
|
96
|
+
userId: payload.userId,
|
|
97
|
+
kind: 'report',
|
|
98
|
+
title: '报表已生成',
|
|
99
|
+
idempotencyKey: `report-done:${payload.reportId}`, // 任务重试不重复通知
|
|
100
|
+
})
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const app = createApp()
|
|
105
|
+
messages.install(app)
|
|
106
|
+
tasks.install(app) // worker 随 app.close() 优雅停止
|
|
107
|
+
|
|
108
|
+
await tasks.enqueue(
|
|
109
|
+
'generate-report',
|
|
110
|
+
{ userId: 'user-1', reportId: 'r1' },
|
|
111
|
+
{ idempotencyKey: 'report:r1' },
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## API 速览
|
|
116
|
+
|
|
117
|
+
`createMessages(options)`
|
|
118
|
+
|
|
119
|
+
| 选项 | 默认 | 说明 |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `store` | 必填 | 三驱动之一或你自己的 `MessageStore` |
|
|
122
|
+
| `bus` | 进程内 | `localMessageBus()`;多副本换 `redisMessageBus()`(见下) |
|
|
123
|
+
| `onSend` | — | 新消息落库后回调;抛错只告警(已持久化),幂等命中不触发 |
|
|
124
|
+
| `routePrefix` | `/messages` | REST 前缀 |
|
|
125
|
+
| `pageSize` | `20`(上限 100) | 列表默认页大小 |
|
|
126
|
+
| `syncLimit` | `100` | 断线补投单次最大条数(`hasMore` 后续页) |
|
|
127
|
+
| `now` / `logger` | — | 可注入时钟、日志器 |
|
|
128
|
+
|
|
129
|
+
`messages` 实例:`send` / `list` / `unread` / `markRead` / `markAllRead` / `routes` / `install` /
|
|
130
|
+
`connections` / `publish` / `ready` / `close` / `migrate`。
|
|
131
|
+
|
|
132
|
+
`send(input)` → `{ message, created }`:`input = { userId, title, kind?, body?, data?, idempotencyKey? }`;
|
|
133
|
+
`created: false` 表示幂等键命中(返回已有消息,**不覆盖**)。推荐键名 `<动作>:<业务主键>`。
|
|
134
|
+
|
|
135
|
+
REST(`messages.install(app)` / `app.mount(messages)` 后;一律 401 守卫 + 仅本人):
|
|
136
|
+
|
|
137
|
+
| 方法 | 路径 | 说明 |
|
|
138
|
+
| --- | --- | --- |
|
|
139
|
+
| GET | `/messages` | 本人消息(`unread`/`kind`/`limit`/`cursor` 查询)→ `{ items, nextCursor, unread }`(按 `(createdAt,id)` 降序) |
|
|
140
|
+
| GET | `/messages/unread` | `{ unread }`(徽标轮询兜底) |
|
|
141
|
+
| POST | `/messages/read` | `{ ids }` → `{ updated }`(幂等) |
|
|
142
|
+
| POST | `/messages/read-all` | → `{ updated }` |
|
|
143
|
+
|
|
144
|
+
## WebSocket 协议
|
|
145
|
+
|
|
146
|
+
连接 `GET /messages/ws`(未认证 401);`?since=<cursor>` 控制补投:
|
|
147
|
+
|
|
148
|
+
- 连接建立后**先**收到 `{ type: 'unread', unread }` 快照,再收实时事件;
|
|
149
|
+
- `since` 非空且合法 → 先推 `{ type: 'sync', items, hasMore, unread }`(**升序**,subscribe-first:
|
|
150
|
+
宁可重投不可丢失);`hasMore` 时用最后一条的游标再连一次续补;
|
|
151
|
+
- `since` 为空/省略 → 视为全量 `sync`;非法游标 → `1008` 关闭;
|
|
152
|
+
- 之后:`send` → `{ type: 'message', message }` + `{ type: 'unread', unread }`;
|
|
153
|
+
`markRead`/`markAllRead` → `{ type: 'unread', unread }`。
|
|
154
|
+
|
|
155
|
+
投递语义是**至少一次**:断线重连可能重投,客户端**按 message.id 去重**即恰好一次。`unread` 是快照值,
|
|
156
|
+
不是增量。
|
|
157
|
+
|
|
158
|
+
## 断线不丢不重(客户端重连配方)
|
|
159
|
+
|
|
160
|
+
客户端记住"最新已见消息"的游标(列表升序取 `items[0]`,或 `sync` 的最后一条),重连时带上:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { encodeCursor } from '@weifuwujs/messages'
|
|
164
|
+
|
|
165
|
+
// 收到列表/同步结果后:
|
|
166
|
+
const latest = page.items[0] // 降序列表的第一条 = 最新
|
|
167
|
+
const since = latest
|
|
168
|
+
? encodeCursor({ id: latest.id, createdAt: new Date(latest.createdAt) })
|
|
169
|
+
: ''
|
|
170
|
+
|
|
171
|
+
// 重连:ws://…/messages/ws?since=<since>,把 sync.items + 实时 message 合并进同一个 Map
|
|
172
|
+
const byId = new Map<string, { id: string }>()
|
|
173
|
+
for (const item of [...sync.items, ...live]) byId.set(item.id, item)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
重复应用同一批事件时 `byId` 不变——这就是"不重复"的全部代价。
|
|
177
|
+
|
|
178
|
+
## 三驱动与运维
|
|
179
|
+
|
|
180
|
+
| 驱动 | 入口 | 建表 | 说明 |
|
|
181
|
+
| --- | --- | --- | --- |
|
|
182
|
+
| memory | `@weifuwujs/messages` 的 `memoryMessageStore()` | — | 测试/单进程 demo |
|
|
183
|
+
| sqlite | `@weifuwujs/messages/sqlite` | 构造即迁移 | 默认 `weifuwu-messages migrate` 目标(`sqlite:data/messages.db`) |
|
|
184
|
+
| postgres | `@weifuwujs/messages/postgres` | 构造即迁移 + advisory lock | 多副本必须:`UNIQUE(user_id, idempotency_key)` 让幂等原子 |
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
weifuwu-messages migrate # sqlite:data/messages.db
|
|
188
|
+
weifuwu-messages migrate --store postgres://user:pw@host/db # 多进程并发安全(pg advisory xact lock)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
redis **不作为消息主存**(不保证持久,丢了就是丢了);它的位置在下面的实时总线。
|
|
192
|
+
|
|
193
|
+
## 多副本部署
|
|
194
|
+
|
|
195
|
+
单进程默认 `localMessageBus()`(进程内 fan-out)。多副本时每个实例都连同一个主存,并把总线换成 Redis:
|
|
196
|
+
|
|
197
|
+
```ts messages-multi
|
|
198
|
+
import { createMessages } from '@weifuwujs/messages'
|
|
199
|
+
import { postgresMessageStore } from '@weifuwujs/messages/postgres'
|
|
200
|
+
import { redisMessageBus } from '@weifuwujs/messages/redis'
|
|
201
|
+
|
|
202
|
+
const bus = redisMessageBus('redis://localhost:6379', { prefix: 'myapp:messages' })
|
|
203
|
+
const messages = createMessages({
|
|
204
|
+
store: postgresMessageStore('postgres://user:pw@localhost:5432/app'),
|
|
205
|
+
bus,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
await messages.ready() // 启动时等一次订阅就绪;之后 send 先发本节点再广播
|
|
209
|
+
// … 服务运行:A 实例 send,持有 WS 的 B 实例也会推送
|
|
210
|
+
await messages.close() // 只关自有连接与自有 bus;注入的 bus 生命周期归调用方
|
|
211
|
+
await bus.close() // 所以注入时由你负责关闭
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- 按用户**按需订阅** `prefix:<userId>`:本节点有该用户连接才订阅,最后一个连接断开后自动退订;
|
|
215
|
+
- `redisMessageBus(url)` 自己创建/关闭连接;传已有 `ioredis` 客户端则只 `duplicate()` 订阅连接,**不关注入的客户端**;
|
|
216
|
+
- 跨节点仍是至少一次:WS 下多节点可能对同一用户重复投递,客户端按 id 去重。
|
|
217
|
+
|
|
218
|
+
## 自定义驱动
|
|
219
|
+
|
|
220
|
+
实现 `MessageStore`(`insert`/`list`/`unreadCount`/`markRead`/`markAllRead`/`migrate?`)后,先用同一套
|
|
221
|
+
契约验证再上线:
|
|
222
|
+
|
|
223
|
+
```ts messages-custom
|
|
224
|
+
import { memoryMessageStore } from '@weifuwujs/messages'
|
|
225
|
+
import { messagesStoreContract } from '@weifuwujs/messages/testing'
|
|
226
|
+
|
|
227
|
+
// 换成你的驱动:messagesStoreContract('my-store', () => myMessageStore())
|
|
228
|
+
messagesStoreContract('my-store', () => memoryMessageStore())
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
契约覆盖:幂等键不覆盖、降序分页稳定(同毫秒按 id)、只影响本人、`markRead` 幂等、未读计数。
|
|
232
|
+
|
|
233
|
+
## 测试辅助(`/testing`)
|
|
234
|
+
|
|
235
|
+
自定义 `MessageStore` / `MessageBus` 驱动与内置驱动共跑同一套契约:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
import {
|
|
239
|
+
messagesStoreContract,
|
|
240
|
+
messageBusContract,
|
|
241
|
+
} from '@weifuwujs/messages/testing'
|
|
242
|
+
|
|
243
|
+
messagesStoreContract('my-store', () => myMessageStore())
|
|
244
|
+
messageBusContract('my-bus', () => myMessageBus())
|
|
245
|
+
// 第三个参数可选:{ skip: true } 或 { skip: '未实现清理' }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
工厂**会被套件多次调用**:每次须返回独立的 store(sqlite 驱动用 `':memory:'` 或唯一路径)——
|
|
249
|
+
复用同一文件会状态泄漏并假红。
|
|
250
|
+
|
|
251
|
+
契约覆盖:insert/get(IDOR 防线)、幂等键、游标分页不重不漏、unread 计数与已读隔离、
|
|
252
|
+
listAfter 补投、watch/unwatch 订阅泄漏回归。
|
|
253
|
+
|
|
254
|
+
## 安全与边界
|
|
255
|
+
|
|
256
|
+
- 所有 store 查询强制 `userId`,越权读取/标记直接表现为"查无此件";REST/WS 未认证 401;
|
|
257
|
+
- `title`/`body` 按**纯文本**存储,渲染层负责转义;`data` 只透传、不参与查询;
|
|
258
|
+
- `app.close()` 会断开全部 WS;`messages.close()` 幂等,注入 bus 时先关 `messages` 再关 bus;
|
|
259
|
+
- 群发/广播是**多次 `send`**(没有 fan-out API),量级大时用 [`./tasks`](./tasks.md) 异步扇出;
|
|
260
|
+
- 明确非目标:IM、跨服务消息总线、消息编辑/撤回、附件。
|
package/dist/bus.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../src/bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,YAAY,CAAA;AAE3D;;;GAGG;AACH,wBAAgB,eAAe,IAAI,UAAU,CAuB5C"}
|
package/dist/bus.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进程内总线:单副本默认。
|
|
3
|
+
* `watch`/`publish` 同步派发(事件不会跨进程,也不做异步缓冲)。
|
|
4
|
+
*/
|
|
5
|
+
export function localMessageBus() {
|
|
6
|
+
const handlers = new Map();
|
|
7
|
+
return {
|
|
8
|
+
publish(userId, event) {
|
|
9
|
+
const set = handlers.get(userId);
|
|
10
|
+
if (!set)
|
|
11
|
+
return;
|
|
12
|
+
for (const handler of [...set])
|
|
13
|
+
handler(event);
|
|
14
|
+
},
|
|
15
|
+
watch(userId, handler) {
|
|
16
|
+
let set = handlers.get(userId);
|
|
17
|
+
if (!set) {
|
|
18
|
+
set = new Set();
|
|
19
|
+
handlers.set(userId, set);
|
|
20
|
+
}
|
|
21
|
+
set.add(handler);
|
|
22
|
+
return () => {
|
|
23
|
+
set.delete(handler);
|
|
24
|
+
if (set.size === 0)
|
|
25
|
+
handlers.delete(userId);
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=bus.js.map
|
package/dist/bus.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bus.js","sourceRoot":"","sources":["../src/bus.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA+C,CAAA;IAEvE,OAAO;QACL,OAAO,CAAC,MAAM,EAAE,KAAK;YACnB,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YAChC,IAAI,CAAC,GAAG;gBAAE,OAAM;YAChB,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,GAAG,CAAC;gBAAE,OAAO,CAAC,KAAK,CAAC,CAAA;QAChD,CAAC;QAED,KAAK,CAAC,MAAM,EAAE,OAAO;YACnB,IAAI,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YAC9B,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAA;gBACf,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;YAC3B,CAAC;YACD,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YAChB,OAAO,GAAG,EAAE;gBACV,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;gBACnB,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;oBAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YAC7C,CAAC,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
function parseArgs(argv) {
|
|
4
|
+
const command = argv[0] ?? 'help';
|
|
5
|
+
let root = process.cwd();
|
|
6
|
+
let store;
|
|
7
|
+
const rest = [];
|
|
8
|
+
for (let index = 1; index < argv.length; index++) {
|
|
9
|
+
const arg = argv[index];
|
|
10
|
+
if (arg === '--root' || arg.startsWith('--root=')) {
|
|
11
|
+
const value = arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : argv[++index];
|
|
12
|
+
if (!value)
|
|
13
|
+
throw new Error('--root 需要提供一个值');
|
|
14
|
+
root = resolve(value);
|
|
15
|
+
}
|
|
16
|
+
else if (arg === '--store' || arg.startsWith('--store=')) {
|
|
17
|
+
const value = arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : argv[++index];
|
|
18
|
+
if (!value)
|
|
19
|
+
throw new Error('--store 需要提供一个值');
|
|
20
|
+
store = value;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
rest.push(arg);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { command, root, store, rest };
|
|
27
|
+
}
|
|
28
|
+
/** `migrate [--store sqlite:<path>]`(CLI 不加载用户代码) */
|
|
29
|
+
async function openMessageStore(options) {
|
|
30
|
+
const spec = options.store ?? 'sqlite:data/messages.db';
|
|
31
|
+
if (spec.startsWith('sqlite:')) {
|
|
32
|
+
const path = resolve(options.root, spec.slice('sqlite:'.length));
|
|
33
|
+
const { sqliteMessageStore } = await import("./stores/sqlite.js");
|
|
34
|
+
const store = sqliteMessageStore(path);
|
|
35
|
+
return { store, close: async () => store.close() };
|
|
36
|
+
}
|
|
37
|
+
if (spec.startsWith('postgres://') || spec.startsWith('postgresql://')) {
|
|
38
|
+
const { postgresMessageStore } = await import("./stores/postgres.js");
|
|
39
|
+
const store = postgresMessageStore(spec);
|
|
40
|
+
return { store, close: () => store.close() };
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`无法识别的 --store:${spec}(支持 sqlite:<path> 与 postgres://…)`);
|
|
43
|
+
}
|
|
44
|
+
async function messagesMigrate(options) {
|
|
45
|
+
const spec = options.store ?? 'sqlite:data/messages.db';
|
|
46
|
+
const { store, close } = await openMessageStore(options);
|
|
47
|
+
try {
|
|
48
|
+
await store.migrate?.();
|
|
49
|
+
console.log(spec.startsWith('sqlite:')
|
|
50
|
+
? `messages 迁移完成:${resolve(options.root, spec.slice('sqlite:'.length))}`
|
|
51
|
+
: `messages 迁移完成:${spec}`);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await close();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function printHelp() {
|
|
58
|
+
console.log(`用法:weifuwu-messages <命令> [选项]
|
|
59
|
+
|
|
60
|
+
命令:
|
|
61
|
+
migrate 迁移 messages 数据表(sqlite/postgres)
|
|
62
|
+
|
|
63
|
+
选项:
|
|
64
|
+
--root <dir> 项目根目录,默认当前目录
|
|
65
|
+
--store <url> 数据源,默认 sqlite:data/messages.db(支持 sqlite:<path> 与 postgres://…)
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
const options = parseArgs(process.argv.slice(2));
|
|
69
|
+
try {
|
|
70
|
+
if (options.command === 'migrate')
|
|
71
|
+
await messagesMigrate(options);
|
|
72
|
+
else if (options.command === 'help')
|
|
73
|
+
printHelp();
|
|
74
|
+
else {
|
|
75
|
+
throw new Error(`未知 messages 子命令:${options.command}(可用:migrate)`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error(error instanceof Error ? error.message : error);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/cursor.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Message } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* 分页游标:`<createdAtMs(36)>.<id>`。
|
|
4
|
+
* 自带时间戳,翻页不依赖游标消息仍然存在(被清理/删除也不影响)。
|
|
5
|
+
*/
|
|
6
|
+
export declare function encodeCursor(message: Pick<Message, 'id' | 'createdAt'>): string;
|
|
7
|
+
export interface DecodedCursor {
|
|
8
|
+
createdAtMs: number;
|
|
9
|
+
id: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function decodeCursor(cursor: string): DecodedCursor | null;
|
|
12
|
+
/** `(createdAt, id)` 双键降序比较(同毫秒稳定) */
|
|
13
|
+
export declare function compareDesc(a: Pick<Message, 'id' | 'createdAt'>, b: Pick<Message, 'id' | 'createdAt'>): number;
|
|
14
|
+
/** 消息是否严格排在游标之后(更早一页) */
|
|
15
|
+
export declare function isAfterCursor(message: Pick<Message, 'id' | 'createdAt'>, cursor: DecodedCursor): boolean;
|
|
16
|
+
/** `(createdAt, id)` 双键升序比较(断线补投按旧→新) */
|
|
17
|
+
export declare function compareAsc(a: Pick<Message, 'id' | 'createdAt'>, b: Pick<Message, 'id' | 'createdAt'>): number;
|
|
18
|
+
/** 消息是否严格排在游标之后(更晚一条,补投用) */
|
|
19
|
+
export declare function isBeyondCursor(message: Pick<Message, 'id' | 'createdAt'>, cursor: DecodedCursor): boolean;
|
|
20
|
+
//# sourceMappingURL=cursor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../src/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEzC;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,WAAW,CAAC,GAAG,MAAM,CAE/E;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,EAAE,EAAE,MAAM,CAAA;CACX;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAMjE;AAED,sCAAsC;AACtC,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,WAAW,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,WAAW,CAAC,GAAG,MAAM,CAK9G;AAED,yBAAyB;AACzB,wBAAgB,aAAa,CAC3B,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,WAAW,CAAC,EAC1C,MAAM,EAAE,aAAa,GACpB,OAAO,CAIT;AAED,yCAAyC;AACzC,wBAAgB,UAAU,CACxB,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,WAAW,CAAC,EACpC,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,WAAW,CAAC,GACnC,MAAM,CAKR;AAED,6BAA6B;AAC7B,wBAAgB,cAAc,CAC5B,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,WAAW,CAAC,EAC1C,MAAM,EAAE,aAAa,GACpB,OAAO,CAIT"}
|
package/dist/cursor.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 分页游标:`<createdAtMs(36)>.<id>`。
|
|
3
|
+
* 自带时间戳,翻页不依赖游标消息仍然存在(被清理/删除也不影响)。
|
|
4
|
+
*/
|
|
5
|
+
export function encodeCursor(message) {
|
|
6
|
+
return `${message.createdAt.getTime().toString(36)}.${message.id}`;
|
|
7
|
+
}
|
|
8
|
+
export function decodeCursor(cursor) {
|
|
9
|
+
const dot = cursor.indexOf('.');
|
|
10
|
+
if (dot <= 0 || dot === cursor.length - 1)
|
|
11
|
+
return null;
|
|
12
|
+
const createdAtMs = Number.parseInt(cursor.slice(0, dot), 36);
|
|
13
|
+
if (!Number.isFinite(createdAtMs))
|
|
14
|
+
return null;
|
|
15
|
+
return { createdAtMs, id: cursor.slice(dot + 1) };
|
|
16
|
+
}
|
|
17
|
+
/** `(createdAt, id)` 双键降序比较(同毫秒稳定) */
|
|
18
|
+
export function compareDesc(a, b) {
|
|
19
|
+
const byTime = b.createdAt.getTime() - a.createdAt.getTime();
|
|
20
|
+
if (byTime !== 0)
|
|
21
|
+
return byTime;
|
|
22
|
+
if (a.id === b.id)
|
|
23
|
+
return 0;
|
|
24
|
+
return a.id < b.id ? 1 : -1;
|
|
25
|
+
}
|
|
26
|
+
/** 消息是否严格排在游标之后(更早一页) */
|
|
27
|
+
export function isAfterCursor(message, cursor) {
|
|
28
|
+
const ms = message.createdAt.getTime();
|
|
29
|
+
if (ms !== cursor.createdAtMs)
|
|
30
|
+
return ms < cursor.createdAtMs;
|
|
31
|
+
return message.id < cursor.id;
|
|
32
|
+
}
|
|
33
|
+
/** `(createdAt, id)` 双键升序比较(断线补投按旧→新) */
|
|
34
|
+
export function compareAsc(a, b) {
|
|
35
|
+
const byTime = a.createdAt.getTime() - b.createdAt.getTime();
|
|
36
|
+
if (byTime !== 0)
|
|
37
|
+
return byTime;
|
|
38
|
+
if (a.id === b.id)
|
|
39
|
+
return 0;
|
|
40
|
+
return a.id < b.id ? -1 : 1;
|
|
41
|
+
}
|
|
42
|
+
/** 消息是否严格排在游标之后(更晚一条,补投用) */
|
|
43
|
+
export function isBeyondCursor(message, cursor) {
|
|
44
|
+
const ms = message.createdAt.getTime();
|
|
45
|
+
if (ms !== cursor.createdAtMs)
|
|
46
|
+
return ms > cursor.createdAtMs;
|
|
47
|
+
return message.id > cursor.id;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../src/cursor.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,OAA0C;IACrE,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,EAAE,EAAE,CAAA;AACpE,CAAC;AAOD,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,GAAG,IAAI,CAAC,IAAI,GAAG,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACtD,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAA;IAC7D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAA;IAC9C,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,CAAA;AACnD,CAAC;AAED,sCAAsC;AACtC,MAAM,UAAU,WAAW,CAAC,CAAoC,EAAE,CAAoC;IACpG,MAAM,MAAM,GAAG,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,CAAA;IAC5D,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAA;IAC/B,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE;QAAE,OAAO,CAAC,CAAA;IAC3B,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7B,CAAC;AAED,yBAAyB;AACzB,MAAM,UAAU,aAAa,CAC3B,OAA0C,EAC1C,MAAqB;IAErB,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,CAAA;IACtC,IAAI,EAAE,KAAK,MAAM,CAAC,WAAW;QAAE,OAAO,EAAE,GAAG,MAAM,CAAC,WAAW,CAAA;IAC7D,OAAO,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,EAAE,CAAA;AAC/B,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,UAAU,CACxB,CAAoC,EACpC,CAAoC;IAEpC,MAAM,MAAM,GAAG,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,CAAA;IAC5D,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAA;IAC/B,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE;QAAE,OAAO,CAAC,CAAA;IAC3B,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7B,CAAC;AAED,6BAA6B;AAC7B,MAAM,UAAU,cAAc,CAC5B,OAA0C,EAC1C,MAAqB;IAErB,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,CAAA;IACtC,IAAI,EAAE,KAAK,MAAM,CAAC,WAAW;QAAE,OAAO,EAAE,GAAG,MAAM,CAAC,WAAW,CAAA;IAC7D,OAAO,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,EAAE,CAAA;AAC/B,CAAC"}
|
package/dist/hub.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WebSocketConnection } from '@weifuwujs/weifuwu';
|
|
2
|
+
import type { MessagesEvent } from './types.ts';
|
|
3
|
+
/** 进程内实时投递(跨节点见阶段 3.3 MessageBus) */
|
|
4
|
+
export interface MessageHub {
|
|
5
|
+
/** 登记连接,返回注销函数(`onClose` 里调用) */
|
|
6
|
+
add(userId: string, socket: WebSocketConnection): () => void;
|
|
7
|
+
/** 只发给该用户的在线连接;连接刚断开时静默跳过 */
|
|
8
|
+
publish(userId: string, event: MessagesEvent): void;
|
|
9
|
+
/** 在线连接数(省略 userId = 全部) */
|
|
10
|
+
connections(userId?: string): number;
|
|
11
|
+
/** 关闭全部连接(`app.close()` 用) */
|
|
12
|
+
closeAll(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function localMessageHub(): MessageHub;
|
|
15
|
+
//# sourceMappingURL=hub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub.d.ts","sourceRoot":"","sources":["../src/hub.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,qCAAqC;AACrC,MAAM,WAAW,UAAU;IACzB,iCAAiC;IACjC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,MAAM,IAAI,CAAA;IAC5D,6BAA6B;IAC7B,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CAAA;IACnD,4BAA4B;IAC5B,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACpC,8BAA8B;IAC9B,QAAQ,IAAI,IAAI,CAAA;CACjB;AAED,wBAAgB,eAAe,IAAI,UAAU,CA+C5C"}
|
package/dist/hub.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function localMessageHub() {
|
|
2
|
+
const byUser = new Map();
|
|
3
|
+
const all = new Set();
|
|
4
|
+
return {
|
|
5
|
+
add(userId, socket) {
|
|
6
|
+
let set = byUser.get(userId);
|
|
7
|
+
if (!set) {
|
|
8
|
+
set = new Set();
|
|
9
|
+
byUser.set(userId, set);
|
|
10
|
+
}
|
|
11
|
+
set.add(socket);
|
|
12
|
+
all.add(socket);
|
|
13
|
+
return () => {
|
|
14
|
+
set.delete(socket);
|
|
15
|
+
if (set.size === 0)
|
|
16
|
+
byUser.delete(userId);
|
|
17
|
+
all.delete(socket);
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
publish(userId, event) {
|
|
21
|
+
const set = byUser.get(userId);
|
|
22
|
+
if (!set || set.size === 0)
|
|
23
|
+
return;
|
|
24
|
+
const payload = JSON.stringify(event);
|
|
25
|
+
for (const socket of [...set]) {
|
|
26
|
+
if (socket.readyState !== 'open')
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
socket.send(payload);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// 连接刚好断开:注销由 onClose 负责,推送静默跳过
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
connections(userId) {
|
|
37
|
+
if (userId === undefined)
|
|
38
|
+
return all.size;
|
|
39
|
+
return byUser.get(userId)?.size ?? 0;
|
|
40
|
+
},
|
|
41
|
+
closeAll() {
|
|
42
|
+
for (const socket of [...all]) {
|
|
43
|
+
if (socket.readyState === 'open')
|
|
44
|
+
socket.close(1001, 'server shutdown');
|
|
45
|
+
}
|
|
46
|
+
all.clear();
|
|
47
|
+
byUser.clear();
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=hub.js.map
|
package/dist/hub.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub.js","sourceRoot":"","sources":["../src/hub.ts"],"names":[],"mappings":"AAeA,MAAM,UAAU,eAAe;IAC7B,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoC,CAAA;IAC1D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAuB,CAAA;IAE1C,OAAO;QACL,GAAG,CAAC,MAAM,EAAE,MAAM;YAChB,IAAI,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YAC5B,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAA;gBACf,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;YACzB,CAAC;YACD,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YACf,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YACf,OAAO,GAAG,EAAE;gBACV,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;gBAClB,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;oBAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;gBACzC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YACpB,CAAC,CAAA;QACH,CAAC;QAED,OAAO,CAAC,MAAM,EAAE,KAAK;YACnB,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YAC9B,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;gBAAE,OAAM;YAClC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;YACrC,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;gBAC9B,IAAI,MAAM,CAAC,UAAU,KAAK,MAAM;oBAAE,SAAQ;gBAC1C,IAAI,CAAC;oBACH,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;gBACtB,CAAC;gBAAC,MAAM,CAAC;oBACP,+BAA+B;gBACjC,CAAC;YACH,CAAC;QACH,CAAC;QAED,WAAW,CAAC,MAAM;YAChB,IAAI,MAAM,KAAK,SAAS;gBAAE,OAAO,GAAG,CAAC,IAAI,CAAA;YACzC,OAAO,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,IAAI,CAAC,CAAA;QACtC,CAAC;QAED,QAAQ;YACN,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;gBAC9B,IAAI,MAAM,CAAC,UAAU,KAAK,MAAM;oBAAE,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAA;YACzE,CAAC;YACD,GAAG,CAAC,KAAK,EAAE,CAAA;YACX,MAAM,CAAC,KAAK,EAAE,CAAA;QAChB,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createMessages } from './messages.ts';
|
|
2
|
+
export { localMessageBus } from './bus.ts';
|
|
3
|
+
export { memoryMessageStore } from './stores/memory.ts';
|
|
4
|
+
export { decodeCursor, encodeCursor } from './cursor.ts';
|
|
5
|
+
export type { CreateMessagesOptions, InsertMessageInput, InstallMessagesOptions, ListAfterPage, ListAfterQuery, ListPage, ListQuery, Message, MessageBus, MessageLogger, MessageStore, Messages, MessagesEvent, MessagesEventMap, SendInput, SendResult, StoreInsertResult, } from './types.ts';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AACxD,YAAY,EACV,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EACb,cAAc,EACd,QAAQ,EACR,SAAS,EACT,OAAO,EACP,UAAU,EACV,aAAa,EACb,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,UAAU,EACV,iBAAiB,GAClB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CreateMessagesOptions, Messages } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* 站内信/通知中心:持久化 + 未读计数 + 本人可见的只读 REST。
|
|
4
|
+
*
|
|
5
|
+
* - `send` 是服务端发送入口(HTTP 不暴露写消息,防越权群发);
|
|
6
|
+
* - 至少一次 + 按 id 去重由客户端/实时层承担,幂等键由 store 唯一约束承担;
|
|
7
|
+
* - 实时推送(WebSocket)见后续阶段,`messages.routes()` 先提供轮询兜底。
|
|
8
|
+
*/
|
|
9
|
+
export declare function createMessages(options: CreateMessagesOptions): Messages;
|
|
10
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,qBAAqB,EAIrB,QAAQ,EAIT,MAAM,YAAY,CAAA;AAqBnB;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,QAAQ,CAsQvE"}
|