weifuwu 0.16.1 → 0.16.3

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 CHANGED
@@ -151,6 +151,7 @@ All use the same pattern — `const m = module(options)` → `app.use('/path', m
151
151
  | `embed()` / `embedMany()` | AI SDK — text embeddings |
152
152
  | `smoothStream()` | AI SDK — smooth streaming middleware |
153
153
  | `openai` / `createOpenAI()` | OpenAI provider for AI SDK |
154
+ | `createHub(options?)` | WebSocket channel hub — `join()`, `leave()`, `broadcast()` with optional Redis pub/sub |
154
155
 
155
156
  ---
156
157
 
@@ -307,6 +308,39 @@ const server = serve(app.handler(), {
307
308
  })
308
309
  ```
309
310
 
311
+ ### Cross-process WebSocket with `createHub`
312
+
313
+ Use `createHub()` to group WebSocket connections into named channels and broadcast to them — works within a single process, and with optional Redis pub/sub across multiple Node.js processes behind a load balancer.
314
+
315
+ ```ts
316
+ import { createHub, serve, Router } from 'weifuwu'
317
+ import { redis } from 'weifuwu'
318
+
319
+ const hub = createHub({ redis }) // omit redis for in-process only
320
+
321
+ const app = new Router().ws('/chat/:room', {
322
+ open(ws, ctx) {
323
+ hub.join(`room:${ctx.params.room}`, ws)
324
+ },
325
+ message(ws, ctx, data) {
326
+ hub.broadcast(`room:${ctx.params.room}`, {
327
+ user: ctx.user?.id,
328
+ text: data.toString(),
329
+ })
330
+ },
331
+ close(ws) {
332
+ hub.leave(ws)
333
+ },
334
+ })
335
+
336
+ serve(app.handler(), {
337
+ port: 3000,
338
+ websocket: app.websocketHandler(),
339
+ })
340
+ ```
341
+
342
+ With Redis configured, `hub.broadcast()` publishes to a Redis channel — all processes subscribed via `createHub({ redis })` receive and forward to their local WebSocket connections.
343
+
310
344
  ---
311
345
 
312
346
  # Middleware
@@ -1326,10 +1360,14 @@ All routes require `ctx.tenant` (set by `t.middleware()`). All queries automatic
1326
1360
  Real-time chat with channels, WebSocket, and agent routing.
1327
1361
 
1328
1362
  ```ts
1329
- import { messager, agent } from 'weifuwu'
1363
+ import { messager, agent, redis } from 'weifuwu'
1330
1364
 
1331
1365
  const agents = agent({ pg })
1332
- const msg = messager({ pg, agents })
1366
+
1367
+ // Single process (no cross-process broadcast):
1368
+ // const msg = messager({ pg, agents })
1369
+ // Multi-process (Redis pub/sub broadcast):
1370
+ const msg = messager({ pg, agents, redis: redis() })
1333
1371
 
1334
1372
  await msg.migrate()
1335
1373
  app.use('/api', msg.router())
package/dist/hub.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { Redis } from './vendor.ts';
2
+ export interface HubOptions {
3
+ redis?: Redis;
4
+ prefix?: string;
5
+ }
6
+ export interface Hub {
7
+ join(key: string, ws: WebSocket): void;
8
+ leave(ws: WebSocket): void;
9
+ broadcast(key: string, data: unknown): void;
10
+ close(): Promise<void>;
11
+ }
12
+ export declare function createHub(opts?: HubOptions): Hub;
package/dist/iii/ws.d.ts CHANGED
@@ -18,6 +18,7 @@ interface WsHandlerDeps {
18
18
  removeStreamSubscriber: (ws: WebSocket) => void;
19
19
  handleInvokeResult: (invocationId: string, result: unknown) => void;
20
20
  handleInvokeError: (invocationId: string, error: string) => void;
21
+ handleInvoke: (ws: WebSocket, invocationId: string, functionId: string, payload: unknown) => void;
21
22
  }
22
23
  export declare function createWsHandler(deps: WsHandlerDeps): {
23
24
  open(_ws: WebSocket, _ctx: Context): void;
package/dist/index.d.ts CHANGED
@@ -38,6 +38,8 @@ export { user } from './user/index.ts';
38
38
  export type { UserOptions, UserData, UserModule, OAuth2Client, } from './user/types.ts';
39
39
  export { redis } from './redis/index.ts';
40
40
  export type { RedisOptions, RedisClient } from './redis/types.ts';
41
+ export { createHub } from './hub.ts';
42
+ export type { Hub, HubOptions } from './hub.ts';
41
43
  export { queue } from './queue/index.ts';
42
44
  export type { QueueOptions, QueueJob, Queue } from './queue/types.ts';
43
45
  export { tenant } from './tenant/index.ts';
package/dist/index.js CHANGED
@@ -3465,6 +3465,62 @@ function redis(opts) {
3465
3465
  return mw;
3466
3466
  }
3467
3467
 
3468
+ // hub.ts
3469
+ function createHub(opts) {
3470
+ const prefix = opts?.prefix ?? "hub:";
3471
+ const channels2 = /* @__PURE__ */ new Map();
3472
+ let redisPub;
3473
+ let redisSub = null;
3474
+ if (opts?.redis) {
3475
+ redisPub = opts.redis;
3476
+ redisSub = opts.redis.duplicate();
3477
+ redisSub.on("message", (rawChannel, rawData) => {
3478
+ if (!rawChannel.startsWith(prefix)) return;
3479
+ const key = rawChannel.slice(prefix.length);
3480
+ const members = channels2.get(key);
3481
+ if (!members) return;
3482
+ for (const ws of members) {
3483
+ try {
3484
+ ws.send(rawData);
3485
+ } catch {
3486
+ }
3487
+ }
3488
+ });
3489
+ }
3490
+ function join5(key, ws) {
3491
+ if (!channels2.has(key)) {
3492
+ channels2.set(key, /* @__PURE__ */ new Set());
3493
+ redisSub?.subscribe(`${prefix}${key}`);
3494
+ }
3495
+ channels2.get(key).add(ws);
3496
+ }
3497
+ function leave(ws) {
3498
+ for (const [, members] of channels2) {
3499
+ members.delete(ws);
3500
+ }
3501
+ }
3502
+ function broadcast(key, data) {
3503
+ const msg = JSON.stringify(data);
3504
+ const members = channels2.get(key);
3505
+ if (members) {
3506
+ for (const ws of members) {
3507
+ try {
3508
+ ws.send(msg);
3509
+ } catch {
3510
+ }
3511
+ }
3512
+ }
3513
+ redisPub?.publish(`${prefix}${key}`, msg);
3514
+ }
3515
+ async function close() {
3516
+ channels2.clear();
3517
+ if (redisSub) {
3518
+ await redisSub.quit();
3519
+ }
3520
+ }
3521
+ return { join: join5, leave, broadcast, close };
3522
+ }
3523
+
3468
3524
  // queue/index.ts
3469
3525
  import { Redis as IORedis2 } from "ioredis";
3470
3526
  import crypto3 from "node:crypto";
@@ -4809,40 +4865,17 @@ function agent(options) {
4809
4865
  }
4810
4866
 
4811
4867
  // messager/ws.ts
4812
- var channels = /* @__PURE__ */ new Map();
4813
4868
  var userConnections = /* @__PURE__ */ new Map();
4869
+ var hub;
4814
4870
  function broadcastToChannel(channelId, data) {
4815
- const members = channels.get(channelId);
4816
- if (!members) return;
4817
- const msg = JSON.stringify(data);
4818
- for (const { ws } of members) {
4819
- try {
4820
- ws.send(msg);
4821
- } catch {
4822
- }
4823
- }
4824
- }
4825
- function subscribe(ws, userId, channelId) {
4826
- if (!channels.has(channelId)) channels.set(channelId, /* @__PURE__ */ new Set());
4827
- channels.get(channelId).add({ ws, userId });
4828
- if (!userConnections.has(userId)) userConnections.set(userId, /* @__PURE__ */ new Set());
4829
- userConnections.get(userId).add(ws);
4830
- }
4831
- function unsubscribe(ws) {
4832
- for (const [, members] of channels) {
4833
- for (const m of members) {
4834
- if (m.ws === ws) {
4835
- members.delete(m);
4836
- break;
4837
- }
4838
- }
4839
- }
4840
- for (const [, conns] of userConnections) {
4841
- conns.delete(ws);
4842
- }
4871
+ hub?.broadcast(`messager:${channelId}`, data);
4843
4872
  }
4844
4873
  function createWSHandler(deps) {
4845
4874
  const { sql: sql2, agents } = deps;
4875
+ hub = createHub({
4876
+ redis: deps.redis,
4877
+ prefix: "messager:"
4878
+ });
4846
4879
  return {
4847
4880
  open(ws, ctx) {
4848
4881
  const userId = ctx.user?.id;
@@ -4871,8 +4904,10 @@ function createWSHandler(deps) {
4871
4904
  RETURNING *
4872
4905
  `;
4873
4906
  const message = row;
4907
+ hub.join(`messager:${channel_id}`, ws);
4908
+ if (!userConnections.has(userId)) userConnections.set(userId, /* @__PURE__ */ new Set());
4909
+ userConnections.get(userId).add(ws);
4874
4910
  broadcastToChannel(channel_id, { type: "message", data: message });
4875
- subscribe(ws, userId, channel_id);
4876
4911
  if (agents) {
4877
4912
  const agentMembers = await sql2`
4878
4913
  SELECT member_id FROM "_channel_members"
@@ -4898,7 +4933,9 @@ function createWSHandler(deps) {
4898
4933
  break;
4899
4934
  }
4900
4935
  case "typing": {
4901
- if (channel_id) subscribe(ws, userId, channel_id);
4936
+ if (channel_id) {
4937
+ hub.join(`messager:${channel_id}`, ws);
4938
+ }
4902
4939
  broadcastToChannel(channel_id, {
4903
4940
  type: "typing",
4904
4941
  channel_id,
@@ -4909,7 +4946,7 @@ function createWSHandler(deps) {
4909
4946
  }
4910
4947
  case "read": {
4911
4948
  if (!channel_id || !last_message_id) return;
4912
- subscribe(ws, userId, channel_id);
4949
+ hub.join(`messager:${channel_id}`, ws);
4913
4950
  await sql2`
4914
4951
  UPDATE "_channel_members"
4915
4952
  SET last_read_id = ${last_message_id}, last_read_at = NOW()
@@ -4926,22 +4963,28 @@ function createWSHandler(deps) {
4926
4963
  }
4927
4964
  },
4928
4965
  close(ws) {
4929
- unsubscribe(ws);
4966
+ hub?.leave(ws);
4967
+ for (const [, conns] of userConnections) {
4968
+ conns.delete(ws);
4969
+ }
4930
4970
  },
4931
4971
  error(ws) {
4932
- unsubscribe(ws);
4972
+ hub?.leave(ws);
4973
+ for (const [, conns] of userConnections) {
4974
+ conns.delete(ws);
4975
+ }
4933
4976
  }
4934
4977
  };
4935
4978
  }
4936
4979
 
4937
4980
  // messager/rest.ts
4938
4981
  function buildRouter3(deps) {
4939
- const { sql: sql2, channels: channels3, members, messages: messages2, agents } = deps;
4982
+ const { sql: sql2, channels: channels2, members, messages: messages2, agents } = deps;
4940
4983
  const r = new Router();
4941
4984
  r.post("/channels", async (req) => {
4942
4985
  const body = await req.json();
4943
4986
  if (!body.name) return Response.json({ error: "name is required" }, { status: 400 });
4944
- const channel = await channels3.insert({
4987
+ const channel = await channels2.insert({
4945
4988
  name: body.name,
4946
4989
  type: body.type || "channel",
4947
4990
  created_by: body.created_by || 1
@@ -4984,14 +5027,14 @@ function buildRouter3(deps) {
4984
5027
  });
4985
5028
  r.get("/channels/:id", async (_req, ctx) => {
4986
5029
  const id2 = parseInt(ctx.params.id, 10);
4987
- const ch = await channels3.read(id2);
5030
+ const ch = await channels2.read(id2);
4988
5031
  if (!ch) return Response.json({ error: "Channel not found" }, { status: 404 });
4989
5032
  const { data: memberRows } = await members.readMany({ channel_id: id2 });
4990
5033
  return Response.json({ channel: ch, members: memberRows });
4991
5034
  });
4992
5035
  r.delete("/channels/:id", async (_req, ctx) => {
4993
5036
  const id2 = parseInt(ctx.params.id, 10);
4994
- await channels3.delete(id2);
5037
+ await channels2.delete(id2);
4995
5038
  return Response.json({ ok: true });
4996
5039
  });
4997
5040
  r.post("/channels/:id/members", async (req, ctx) => {
@@ -5092,8 +5135,9 @@ function messager(options) {
5092
5135
  const pg = options.pg;
5093
5136
  const sql2 = pg.sql;
5094
5137
  const agents = options.agents;
5138
+ const redis2 = options.redis;
5095
5139
  const base = new PgModule(pg);
5096
- const channels3 = pg.table("_channels", {
5140
+ const channels2 = pg.table("_channels", {
5097
5141
  id: serial("id").primaryKey(),
5098
5142
  tenant_id: text("tenant_id"),
5099
5143
  name: text("name").notNull().default(""),
@@ -5125,16 +5169,16 @@ function messager(options) {
5125
5169
  });
5126
5170
  return {
5127
5171
  migrate: async () => {
5128
- await channels3.create();
5129
- await channels3.createIndex("tenant_id");
5172
+ await channels2.create();
5173
+ await channels2.createIndex("tenant_id");
5130
5174
  await members.create();
5131
5175
  await members.createIndex("member_id");
5132
5176
  await members.createIndex(["channel_id", "member_id", "member_type"], { unique: true });
5133
5177
  await messages2.create();
5134
5178
  await messages2.createIndex(["channel_id", "created_at"], { desc: true });
5135
5179
  },
5136
- router: () => buildRouter3({ sql: sql2, channels: channels3, members, messages: messages2, agents }),
5137
- wsHandler: () => createWSHandler({ sql: sql2, agents }),
5180
+ router: () => buildRouter3({ sql: sql2, channels: channels2, members, messages: messages2, agents }),
5181
+ wsHandler: () => createWSHandler({ sql: sql2, agents, redis: redis2 }),
5138
5182
  async send(channelId, content, opts) {
5139
5183
  const msg = await messages2.insert({
5140
5184
  channel_id: channelId,
@@ -7013,7 +7057,7 @@ function logdb(options) {
7013
7057
  import crypto5 from "node:crypto";
7014
7058
 
7015
7059
  // iii/stream.ts
7016
- var channels2 = /* @__PURE__ */ new Map();
7060
+ var channels = /* @__PURE__ */ new Map();
7017
7061
  function notify(stream, group, item, event, data) {
7018
7062
  const keys = [
7019
7063
  `${stream}`,
@@ -7022,7 +7066,7 @@ function notify(stream, group, item, event, data) {
7022
7066
  ];
7023
7067
  const msg = JSON.stringify({ type: "stream", stream_name: stream, group_id: group, item_id: item, event, data });
7024
7068
  for (const key of keys) {
7025
- const subs = channels2.get(key);
7069
+ const subs = channels.get(key);
7026
7070
  if (!subs) continue;
7027
7071
  for (const ws of subs) {
7028
7072
  try {
@@ -7347,14 +7391,14 @@ function createStream(opts) {
7347
7391
  ...store,
7348
7392
  subscribe(ws, sub) {
7349
7393
  const key = sub.item_id ? `${sub.stream_name}:${sub.group_id}:${sub.item_id}` : sub.group_id ? `${sub.stream_name}:${sub.group_id}` : sub.stream_name;
7350
- if (!channels2.has(key)) channels2.set(key, /* @__PURE__ */ new Set());
7351
- channels2.get(key).add(ws);
7394
+ if (!channels.has(key)) channels.set(key, /* @__PURE__ */ new Set());
7395
+ channels.get(key).add(ws);
7352
7396
  if (redisSub && sub.stream_name) {
7353
7397
  redisSub.subscribe(`iii:stream:${sub.stream_name}`);
7354
7398
  }
7355
7399
  },
7356
7400
  unsubscribe(ws) {
7357
- for (const [, subs] of channels2) subs.delete(ws);
7401
+ for (const [, subs] of channels) subs.delete(ws);
7358
7402
  },
7359
7403
  async migrate() {
7360
7404
  if (opts?.pg) {
@@ -7428,6 +7472,10 @@ function createWsHandler(deps) {
7428
7472
  if (workerId) deps.unregisterRemoteTrigger(workerId, msg.function_id);
7429
7473
  break;
7430
7474
  }
7475
+ case "invoke": {
7476
+ deps.handleInvoke(ws, msg.invocation_id, msg.function_id, msg.payload);
7477
+ break;
7478
+ }
7431
7479
  case "invoke_result": {
7432
7480
  deps.handleInvokeResult(msg.invocation_id, msg.result);
7433
7481
  break;
@@ -7667,6 +7715,23 @@ function iii(opts = {}) {
7667
7715
  p.reject(new Error(error));
7668
7716
  pending.delete(invocationId);
7669
7717
  }
7718
+ },
7719
+ handleInvoke(ws, invocationId, functionId, payload) {
7720
+ const fn = functions.get(functionId);
7721
+ if (!fn) {
7722
+ ws.send(JSON.stringify({
7723
+ type: "invoke_error",
7724
+ invocation_id: invocationId,
7725
+ error: `Function "${functionId}" not found`
7726
+ }));
7727
+ return;
7728
+ }
7729
+ const ctx = { engine: module, functionId, workerName: fn.workerName };
7730
+ Promise.resolve(fn.handler(payload, ctx)).then((result) => {
7731
+ ws.send(JSON.stringify({ type: "invoke_result", invocation_id: invocationId, result }));
7732
+ }).catch((err) => {
7733
+ ws.send(JSON.stringify({ type: "invoke_error", invocation_id: invocationId, error: err.message }));
7734
+ });
7670
7735
  }
7671
7736
  });
7672
7737
  function doTrigger(request) {
@@ -7976,6 +8041,7 @@ export {
7976
8041
  auth,
7977
8042
  compress,
7978
8043
  cors,
8044
+ createHub,
7979
8045
  createOpenAI,
7980
8046
  createSSEStream,
7981
8047
  createTestServer,
@@ -1,9 +1,11 @@
1
1
  import type { AgentModule } from '../agent/types.ts';
2
2
  import type { PostgresClient } from '../postgres/types.ts';
3
+ import type { Redis } from '../vendor.ts';
3
4
  export interface MessagerOptions {
4
5
  pg: PostgresClient;
5
6
  agents?: AgentModule;
6
7
  webhookTimeout?: number;
8
+ redis?: Redis;
7
9
  }
8
10
  export interface Channel {
9
11
  id: number;
@@ -3,6 +3,7 @@ import type { AgentModule } from '../agent/types.ts';
3
3
  interface WSDeps {
4
4
  sql: Sql<{}>;
5
5
  agents?: AgentModule;
6
+ redis?: import('../vendor.ts').Redis;
6
7
  }
7
8
  export declare function broadcastToChannel(channelId: number, data: any): void;
8
9
  export declare function createWSHandler(deps: WSDeps): any;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.16.1",
3
+ "version": "0.16.3",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",