@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,248 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, test } from 'node:test';
3
+ import { encodeCursor } from "./cursor.js";
4
+ const T0 = new Date('2026-01-01T00:00:00.000Z');
5
+ const at = (offsetMs) => new Date(T0.getTime() + offsetMs);
6
+ function baseInsert(overrides = {}) {
7
+ return {
8
+ userId: 'u1',
9
+ kind: 'default',
10
+ title: 'a',
11
+ body: null,
12
+ data: null,
13
+ idempotencyKey: null,
14
+ createdAt: T0,
15
+ ...overrides,
16
+ };
17
+ }
18
+ /**
19
+ * 消息存储契约套件:memory / sqlite / postgres / redis 与自定义驱动共用。
20
+ *
21
+ * ```ts
22
+ * import { messagesStoreContract } from '@weifuwujs/messages/testing'
23
+ * messagesStoreContract('my-store', () => myMessageStore())
24
+ * ```
25
+ */
26
+ export function messagesStoreContract(name, create, options = {}) {
27
+ describe(`message store contract (${name})`, { skip: options.skip }, () => {
28
+ test('insert/get:字段落库、get 必须带 userId(IDOR 防线)', async () => {
29
+ const store = await create();
30
+ const first = await store.insert(baseInsert({ title: '欢迎', data: { href: '/x' } }));
31
+ assert.equal(first.created, true);
32
+ assert.equal(first.message.userId, 'u1');
33
+ assert.equal(first.message.kind, 'default');
34
+ assert.equal(first.message.title, '欢迎');
35
+ assert.deepEqual(first.message.data, { href: '/x' });
36
+ assert.equal(first.message.readAt, null);
37
+ assert.equal(first.message.createdAt.getTime(), T0.getTime());
38
+ const loaded = await store.get(first.message.id, 'u1');
39
+ assert.equal(loaded?.id, first.message.id);
40
+ assert.deepEqual(loaded?.data, { href: '/x' });
41
+ assert.equal(await store.get(first.message.id, 'u2'), null, '他人 get 必须为 null');
42
+ assert.equal(await store.get('missing', 'u1'), null);
43
+ });
44
+ test('insert 幂等键:重复返回已有且不覆盖;不同用户同键互不影响', async () => {
45
+ const store = await create();
46
+ const first = await store.insert(baseInsert({ title: '原消息', idempotencyKey: 'k1' }));
47
+ const again = await store.insert(baseInsert({ title: '改了', idempotencyKey: 'k1' }));
48
+ assert.equal(again.created, false);
49
+ assert.equal(again.message.id, first.message.id);
50
+ assert.equal(again.message.title, '原消息', '幂等命中不得覆盖');
51
+ assert.equal((await store.list({ userId: 'u1' })).items.length, 1);
52
+ const other = await store.insert(baseInsert({ userId: 'u2', idempotencyKey: 'k1' }));
53
+ assert.equal(other.created, true);
54
+ assert.notEqual(other.message.id, first.message.id);
55
+ });
56
+ test('list:新→旧、unread/kind 过滤、游标续页不重不漏', async () => {
57
+ const store = await create();
58
+ for (let index = 1; index <= 5; index++) {
59
+ await store.insert(baseInsert({ title: `m${index}`, createdAt: at(index * 1000) }));
60
+ }
61
+ await store.insert(baseInsert({
62
+ title: 'k',
63
+ kind: 'task-done',
64
+ createdAt: at(6000),
65
+ }));
66
+ const page1 = await store.list({ userId: 'u1', limit: 2 });
67
+ assert.deepEqual(page1.items.map((item) => item.title), ['k', 'm5']);
68
+ assert.ok(page1.nextCursor);
69
+ const page2 = await store.list({
70
+ userId: 'u1',
71
+ limit: 2,
72
+ cursor: page1.nextCursor,
73
+ });
74
+ assert.deepEqual(page2.items.map((item) => item.title), ['m4', 'm3']);
75
+ assert.ok(page2.nextCursor);
76
+ const page3 = await store.list({
77
+ userId: 'u1',
78
+ limit: 2,
79
+ cursor: page2.nextCursor,
80
+ });
81
+ assert.deepEqual(page3.items.map((item) => item.title), ['m2', 'm1']);
82
+ assert.equal(page3.nextCursor, null, '最后一页 nextCursor 为 null');
83
+ const kind = await store.list({ userId: 'u1', kind: 'task-done' });
84
+ assert.deepEqual(kind.items.map((item) => item.title), ['k']);
85
+ assert.equal(kind.nextCursor, null);
86
+ assert.deepEqual((await store.list({ userId: 'u2' })).items, []);
87
+ });
88
+ test('list:同毫秒消息游标分页不重不漏(双键稳定)', async () => {
89
+ const store = await create();
90
+ for (let index = 1; index <= 5; index++) {
91
+ await store.insert(baseInsert({ title: `same-${index}`, createdAt: T0 }));
92
+ }
93
+ const seen = [];
94
+ let cursor = null;
95
+ for (let page = 0; page < 5; page++) {
96
+ const result = await store.list({ userId: 'u1', limit: 2, cursor });
97
+ seen.push(...result.items.map((item) => item.id));
98
+ cursor = result.nextCursor;
99
+ if (cursor === null)
100
+ break;
101
+ }
102
+ assert.equal(seen.length, 5);
103
+ assert.equal(new Set(seen).size, 5, '同毫秒分页不得重复');
104
+ });
105
+ test('unreadCount/markRead/markAllRead:按人隔离、幂等、只影响本人', async () => {
106
+ const store = await create();
107
+ const mine1 = await store.insert(baseInsert({ title: 'mine-1' }));
108
+ const mine2 = await store.insert(baseInsert({ title: 'mine-2' }));
109
+ const theirs = await store.insert(baseInsert({ userId: 'u2', title: 'theirs' }));
110
+ assert.equal(await store.unreadCount('u1'), 2);
111
+ assert.equal(await store.unreadCount('u2'), 1);
112
+ assert.equal(await store.unreadCount('nobody'), 0);
113
+ assert.equal(await store.markRead('u1', [theirs.message.id], at(1000)), 0, '越权 ids 不生效');
114
+ assert.equal(await store.unreadCount('u2'), 1);
115
+ assert.equal(await store.markRead('u1', [mine1.message.id, 'missing'], at(1000)), 1);
116
+ assert.equal(await store.markRead('u1', [mine1.message.id], at(2000)), 0, '重复标记返回 0');
117
+ assert.equal(await store.unreadCount('u1'), 1);
118
+ assert.equal((await store.get(mine1.message.id, 'u1'))?.readAt?.getTime(), at(1000).getTime());
119
+ const unreadOnly = await store.list({ userId: 'u1', unreadOnly: true });
120
+ assert.deepEqual(unreadOnly.items.map((item) => item.title), ['mine-2']);
121
+ assert.equal(await store.markAllRead('u1', at(3000)), 1);
122
+ assert.equal(await store.unreadCount('u1'), 0);
123
+ assert.equal(await store.markAllRead('u1', at(4000)), 0, '重复全部已读返回 0');
124
+ assert.equal(await store.unreadCount('u2'), 1, 'u2 未读不受影响');
125
+ assert.ok(mine2.message.id);
126
+ });
127
+ test('listAfter:游标之后升序补投、hasMore 续接、仅本人、无效游标报错', async () => {
128
+ const store = await create();
129
+ for (let index = 1; index <= 5; index++) {
130
+ await store.insert(baseInsert({ title: `m${index}`, createdAt: at(index * 1000) }));
131
+ }
132
+ await store.insert(baseInsert({ userId: 'u2', title: 'theirs', createdAt: at(60_000) }));
133
+ const page1 = await store.listAfter({ userId: 'u1', cursor: null, limit: 2 });
134
+ assert.deepEqual(page1.items.map((item) => item.title), ['m1', 'm2']);
135
+ assert.equal(page1.hasMore, true);
136
+ const page2 = await store.listAfter({
137
+ userId: 'u1',
138
+ cursor: encodeCursor(page1.items[1]),
139
+ limit: 2,
140
+ });
141
+ assert.deepEqual(page2.items.map((item) => item.title), ['m3', 'm4']);
142
+ assert.equal(page2.hasMore, true);
143
+ const page3 = await store.listAfter({
144
+ userId: 'u1',
145
+ cursor: encodeCursor(page2.items[1]),
146
+ limit: 2,
147
+ });
148
+ assert.deepEqual(page3.items.map((item) => item.title), ['m5']);
149
+ assert.equal(page3.hasMore, false);
150
+ const empty = await store.listAfter({
151
+ userId: 'u1',
152
+ cursor: encodeCursor(page3.items[0]),
153
+ limit: 2,
154
+ });
155
+ assert.deepEqual(empty.items, []);
156
+ assert.equal(empty.hasMore, false);
157
+ const theirs = await store.listAfter({ userId: 'u2', cursor: null, limit: 10 });
158
+ assert.deepEqual(theirs.items.map((item) => item.title), ['theirs']);
159
+ await assert.rejects(() => store.listAfter({ userId: 'u1', cursor: 'bogus', limit: 2 }), /invalid cursor/);
160
+ });
161
+ test('listAfter:同毫秒升序稳定,续接不重不漏', async () => {
162
+ const store = await create();
163
+ for (let index = 0; index < 5; index++) {
164
+ await store.insert(baseInsert({ title: `s${index}`, createdAt: T0 }));
165
+ }
166
+ const seen = [];
167
+ let cursor = null;
168
+ for (;;) {
169
+ const page = await store.listAfter({ userId: 'u1', cursor, limit: 2 });
170
+ seen.push(...page.items.map((item) => item.id));
171
+ if (!page.hasMore)
172
+ break;
173
+ assert.ok(page.items.length > 0, 'hasMore 时不得返回空页');
174
+ cursor = encodeCursor(page.items[page.items.length - 1]);
175
+ }
176
+ assert.equal(seen.length, 5);
177
+ assert.equal(new Set(seen).size, 5, '同毫秒升序续接不得重复');
178
+ });
179
+ });
180
+ }
181
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
182
+ async function waitFor(condition, timeoutMs = 3000) {
183
+ const deadline = Date.now() + timeoutMs;
184
+ while (!condition()) {
185
+ if (Date.now() > deadline)
186
+ throw new Error('waitFor 超时');
187
+ await sleep(10);
188
+ }
189
+ }
190
+ /**
191
+ * 消息总线契约套件:local / redis 与自定义总线共用。
192
+ *
193
+ * ```ts
194
+ * import { messageBusContract } from '@weifuwujs/messages/testing'
195
+ * messageBusContract('my-bus', () => myMessageBus())
196
+ * ```
197
+ */
198
+ export function messageBusContract(name, create, options = {}) {
199
+ describe(`message bus contract (${name})`, { skip: options.skip }, () => {
200
+ test('watch/publish:多 watcher 全收、按用户隔离、无 watcher 不抛', async () => {
201
+ const bus = await create();
202
+ try {
203
+ const u1a = [];
204
+ const u1b = [];
205
+ const u2 = [];
206
+ bus.watch('u1', (event) => u1a.push(event));
207
+ bus.watch('u1', (event) => u1b.push(event));
208
+ bus.watch('u2', (event) => u2.push(event));
209
+ await bus.ready?.();
210
+ const first = { type: 'unread', unread: 1 };
211
+ await bus.publish('u1', first);
212
+ await waitFor(() => u1a.length === 1 && u1b.length === 1);
213
+ assert.deepEqual(u1a[0], first);
214
+ assert.deepEqual(u1b[0], first);
215
+ assert.equal(u2.length, 0, '不同用户不得串台');
216
+ const second = { type: 'unread', unread: 2 };
217
+ await bus.publish('u2', second);
218
+ await waitFor(() => u2.length === 1);
219
+ assert.deepEqual(u2[0], second);
220
+ await bus.publish('nobody', second);
221
+ }
222
+ finally {
223
+ await bus.close?.();
224
+ }
225
+ });
226
+ test('unwatch:退订后旧 handler 不得再收(订阅泄漏回归)', async () => {
227
+ const bus = await create();
228
+ try {
229
+ const seen = [];
230
+ const stop = bus.watch('u1', (event) => seen.push(event));
231
+ await bus.ready?.();
232
+ await bus.publish('u1', { type: 'unread', unread: 1 });
233
+ await waitFor(() => seen.length === 1);
234
+ stop();
235
+ bus.watch('u1', (event) => seen.push(event));
236
+ await bus.ready?.();
237
+ await bus.publish('u1', { type: 'unread', unread: 2 });
238
+ await waitFor(() => seen.length === 2);
239
+ await sleep(150);
240
+ assert.equal(seen.length, 2, 'unwatch 后旧 handler 泄漏,收到重复事件');
241
+ }
242
+ finally {
243
+ await bus.close?.();
244
+ }
245
+ });
246
+ });
247
+ }
248
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ import type { App, MountableRoute } from '@weifuwujs/weifuwu';
2
+ /** 站内信消息(userId 是归属人;data 必须 JSON 可序列化) */
3
+ export interface Message {
4
+ id: string;
5
+ userId: string;
6
+ /** 分类,如 `welcome` / `task-done` */
7
+ kind: string;
8
+ title: string;
9
+ body: string | null;
10
+ /** 深链/附加数据(JSON 可序列化) */
11
+ data: unknown;
12
+ readAt: Date | null;
13
+ /** (userId, key) 唯一;重复 send 返回已有消息 */
14
+ idempotencyKey: string | null;
15
+ createdAt: Date;
16
+ }
17
+ /** store.insert 入参:createdAt 由门面注入的时钟决定(测试确定性) */
18
+ export interface InsertMessageInput {
19
+ userId: string;
20
+ kind: string;
21
+ title: string;
22
+ body: string | null;
23
+ data: unknown;
24
+ idempotencyKey: string | null;
25
+ createdAt: Date;
26
+ }
27
+ export interface StoreInsertResult {
28
+ message: Message;
29
+ created: boolean;
30
+ }
31
+ export interface ListQuery {
32
+ userId: string;
33
+ unreadOnly?: boolean;
34
+ kind?: string;
35
+ /** 默认 20(由门面填充),store 按 1..100 处理 */
36
+ limit?: number;
37
+ /** 上一页的 `nextCursor`;无效游标应抛错 */
38
+ cursor?: string | null;
39
+ }
40
+ export interface ListPage {
41
+ items: Message[];
42
+ /** 非 null 表示还有更早的一页 */
43
+ nextCursor: string | null;
44
+ }
45
+ /** 断线补投:查询游标之后(更晚)的消息 */
46
+ export interface ListAfterQuery {
47
+ userId: string;
48
+ /** null = 从最早开始全量;无效游标应招错 */
49
+ cursor: string | null;
50
+ limit: number;
51
+ }
52
+ export interface ListAfterPage {
53
+ /** 升序(旧→新),便于客户端按序播放 */
54
+ items: Message[];
55
+ /** true = 还有更晚的消息(用最后一条的游标续接) */
56
+ hasMore: boolean;
57
+ }
58
+ /**
59
+ * 消息存储契约(memory / sqlite / postgres / redis 同一套)。
60
+ *
61
+ * 语义要点:
62
+ * - 查询一律带 `userId`(IDOR 防线);`get` 非本人返回 null;
63
+ * - `insert` 幂等键命中返回已有消息(created=false),不覆盖;
64
+ * - `list` 按 `(createdAt, id)` 降序(新→旧),同毫秒也稳定;
65
+ * - `markRead`/`markAllRead` 只写本人消息且幂等,返回实际更新数。
66
+ */
67
+ export interface MessageStore {
68
+ insert(input: InsertMessageInput): Promise<StoreInsertResult>;
69
+ get(id: string, userId: string): Promise<Message | null>;
70
+ list(query: ListQuery): Promise<ListPage>;
71
+ /** 游标之后(更晚)的消息,升序;断线补投用 */
72
+ listAfter(query: ListAfterQuery): Promise<ListAfterPage>;
73
+ unreadCount(userId: string): Promise<number>;
74
+ markRead(userId: string, ids: string[], now: Date): Promise<number>;
75
+ markAllRead(userId: string, now: Date): Promise<number>;
76
+ /** 可选:建表/迁移(sqlite/postgres 实现;memory 不需要) */
77
+ migrate?(): Promise<void>;
78
+ }
79
+ export interface SendInput {
80
+ userId: string;
81
+ title: string;
82
+ kind?: string;
83
+ body?: string | null;
84
+ data?: unknown;
85
+ idempotencyKey?: string | null;
86
+ }
87
+ export interface SendResult {
88
+ message: Message;
89
+ /** false = 幂等键命中已有消息 */
90
+ created: boolean;
91
+ }
92
+ /** WS 推送事件(`message` 新消息 / `unread` 徽标变化 / `sync` 断线补投快照) */
93
+ export interface MessagesEventMap {
94
+ message: {
95
+ type: 'message';
96
+ message: Message;
97
+ };
98
+ unread: {
99
+ type: 'unread';
100
+ unread: number;
101
+ };
102
+ sync: {
103
+ type: 'sync';
104
+ /** 升序(旧→新),客户端按 id 去重后播放 */
105
+ items: Message[];
106
+ hasMore: boolean;
107
+ unread: number;
108
+ };
109
+ }
110
+ export type MessagesEvent = MessagesEventMap[keyof MessagesEventMap];
111
+ /**
112
+ * 跨节点推送总线(`localMessageBus()` 默认;`redisMessageBus()` 多副本)。
113
+ * 语义:至少一次、可乱序;`unread` 是快照值,`message` 由客户端按 id 去重。
114
+ */
115
+ export interface MessageBus {
116
+ /** 发布到所有节点(含本节点);失败不应影响业务写入 */
117
+ publish(userId: string, event: MessagesEvent): void | Promise<void>;
118
+ /** 本节点开始接收该用户事件;返回停止函数 */
119
+ watch(userId: string, handler: (event: MessagesEvent) => void): () => void;
120
+ /** 等待订阅就绪(redis 连接);in-process 可省略 */
121
+ ready?(): void | Promise<void>;
122
+ close?(): void | Promise<void>;
123
+ }
124
+ export interface InstallMessagesOptions {
125
+ /** WS 端点路径,默认 `/messages/ws` */
126
+ wsPath?: string;
127
+ /** 是否同时 `app.mount(messages)`,默认 true */
128
+ rest?: boolean;
129
+ }
130
+ export interface MessageLogger {
131
+ warn(message: string): void;
132
+ error(message: string): void;
133
+ }
134
+ export interface CreateMessagesOptions {
135
+ store: MessageStore;
136
+ /** 实时总线;默认进程内 `localMessageBus()`(多副本用 `redisMessageBus()`) */
137
+ bus?: MessageBus;
138
+ /** 新消息创建后回调;抛错只告警(消息已持久化),幂等命中不触发 */
139
+ onSend?: (message: Message) => void | Promise<void>;
140
+ logger?: MessageLogger;
141
+ /** 可注入时钟(测试用) */
142
+ now?: () => Date;
143
+ /** REST 前缀,默认 `/messages` */
144
+ routePrefix?: string;
145
+ /** 分页默认页大小,默认 20(上限 100) */
146
+ pageSize?: number;
147
+ /** 断线补投单次最大条数,默认 100 */
148
+ syncLimit?: number;
149
+ }
150
+ export interface Messages {
151
+ store: MessageStore;
152
+ send(input: SendInput): Promise<SendResult>;
153
+ list(query: ListQuery): Promise<ListPage>;
154
+ unread(userId: string): Promise<number>;
155
+ markRead(userId: string, ids: string[]): Promise<number>;
156
+ markAllRead(userId: string): Promise<number>;
157
+ /** 挂到 `app.mount(messages)` 的只读 REST 路由(未认证 401) */
158
+ routes(): MountableRoute[];
159
+ /** 一条线接线:`app.mount(messages)` + WS 端点(未认证升级 401),并随 `app.close()` 收连接 */
160
+ install(app: App<any>, options?: InstallMessagesOptions): Messages;
161
+ /** 在线连接数(省略 userId = 全部) */
162
+ connections(userId?: string): number;
163
+ /** 手动推送(自定义流程/测试);正常路径由 send / markRead 自动推送 */
164
+ publish(userId: string, event: MessagesEvent): Promise<void>;
165
+ /** 等待总线订阅就绪(redis 连接后调一次) */
166
+ ready(): Promise<void>;
167
+ /** 停止订阅、关闭本实例拥有的总线与全部连接;可重复调(幂等) */
168
+ close(): Promise<void>;
169
+ migrate(): Promise<void>;
170
+ }
171
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "@weifuwujs/messages",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "weifuwu 站内信:幂等发送、REST 收件箱、WS 实时 + 断线 since 补投去重、memory/sqlite/postgres 契约套件、可插拔总线",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=24.0.0"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./sqlite": {
16
+ "types": "./dist/sqlite.d.ts",
17
+ "import": "./dist/sqlite.js",
18
+ "default": "./dist/sqlite.js"
19
+ },
20
+ "./postgres": {
21
+ "types": "./dist/postgres.d.ts",
22
+ "import": "./dist/postgres.js",
23
+ "default": "./dist/postgres.js"
24
+ },
25
+ "./redis": {
26
+ "types": "./dist/redis.d.ts",
27
+ "import": "./dist/redis.js",
28
+ "default": "./dist/redis.js"
29
+ },
30
+ "./testing": {
31
+ "types": "./dist/testing.d.ts",
32
+ "import": "./dist/testing.js",
33
+ "default": "./dist/testing.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "templates"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "typecheck": "tsc -p tsconfig.json",
43
+ "test": "node --test --test-force-exit \"test/**/*.test.ts\"",
44
+ "prepublishOnly": "npm run typecheck && npm run build && npm test"
45
+ },
46
+ "peerDependencies": {
47
+ "@weifuwujs/weifuwu": ">=1 <2",
48
+ "postgres": ">=3.4.0",
49
+ "ioredis": ">=5.0.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "postgres": {
53
+ "optional": true
54
+ },
55
+ "ioredis": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "devDependencies": {
60
+ "@weifuwujs/weifuwu": "file:../..",
61
+ "@weifuwujs/tasks": "file:../tasks"
62
+ },
63
+ "bin": {
64
+ "weifuwu-messages": "./dist/cli.js"
65
+ }
66
+ }
@@ -0,0 +1,13 @@
1
+ import { createMessages } from '@weifuwujs/weifuwu/messages'
2
+ import { sqliteMessageStore } from '@weifuwujs/weifuwu/messages/sqlite'
3
+
4
+ /**
5
+ * 站内信:默认 sqlite(构造自动建表,零迁移步骤)。
6
+ *
7
+ * - 换 postgres:`postgresMessageStore(process.env.DATABASE_URL!)`(`./messages/postgres`);
8
+ * - 多副本实时:`bus: redisMessageBus(process.env.REDIS_URL!, { prefix: 'ww:messages' })`
9
+ * (`./messages/redis`;注入的 bus 由你负责 close)。
10
+ */
11
+ export const messages = createMessages({
12
+ store: sqliteMessageStore('data/messages.db'),
13
+ })
@@ -0,0 +1,51 @@
1
+ import { Link } from 'react-router'
2
+ import type { Route } from './+types/home'
3
+ import { authUserContext, greetingContext } from '~/context.ts'
4
+ import { messages } from '~/messages.ts'
5
+
6
+ export function meta() {
7
+ return [{ title: '首页' }]
8
+ }
9
+
10
+ export async function loader({ context }: Route.LoaderArgs) {
11
+ const user = context.get(authUserContext)
12
+ if (!user) {
13
+ return { greeting: context.get(greetingContext) ?? 'missing-context', unread: 0 }
14
+ }
15
+ // 首访欢迎信:幂等键让重复访问/重试只落一条(站内信核心保证的现场演示)
16
+ await messages.send({
17
+ userId: user.id,
18
+ kind: 'welcome',
19
+ title: '欢迎使用站内信',
20
+ body: '这是你的第一条消息;到通知页可以标记已读,并看到实时推送。',
21
+ idempotencyKey: `welcome:${user.id}`,
22
+ })
23
+ return {
24
+ greeting: context.get(greetingContext) ?? 'missing-context',
25
+ unread: await messages.unread(user.id),
26
+ }
27
+ }
28
+
29
+ export default function Home({ loaderData }: Route.ComponentProps) {
30
+ return (
31
+ <main className="mx-auto max-w-xl p-8">
32
+ <h1 className="text-3xl font-bold text-blue-600">
33
+ Hello from React Router
34
+ </h1>
35
+ <p className="mt-2 text-gray-600">{loaderData.greeting}</p>
36
+ <Link
37
+ to="/notifications"
38
+ data-testid="messages-link"
39
+ data-unread={loaderData.unread}
40
+ className="mt-6 inline-flex items-center gap-2 border px-3 py-1.5 text-sm"
41
+ >
42
+ 消息
43
+ {loaderData.unread > 0 ? (
44
+ <span className="rounded-full bg-red-600 px-2 text-white">
45
+ {loaderData.unread}
46
+ </span>
47
+ ) : null}
48
+ </Link>
49
+ </main>
50
+ )
51
+ }